From 3bbc81554b864e73a32680db5ff4623eb8228dba Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Tue, 19 May 2026 09:15:25 +0900 Subject: [PATCH 01/22] Python: Improve the handling of intermediate outputs for workflows and orchestrations (#5623) * Improve the handling of intermediate outputs for workflows and orchestrations * Address PR review feedback on intermediate output forwarding - Switch workflow.as_agent() forwarding to an explicit allowlist of {output, intermediate, data, request_info} so orchestration-internal events (group_chat, handoff_sent, magentic_orchestrator) stay inside the workflow instead of leaking into agent responses via str(data) coercion. - Stop raising on intermediate AgentResponseUpdate in non-streaming run(); surface the partial as a Message with text_reasoning content. The defensive raise still applies to terminal output events, where Update payloads would corrupt message ordering. - Extend the DevUI workflow-event mapper so intermediate yields wrapping plain strings, Messages, and list[Message] render as visible output items instead of generic completed-trace events. - Add orchestration coverage for GroupChat, Handoff, and Magentic builders (default vs intermediate_outputs=True; structural where end-to-end is heavy). * Lift output-designation policy into a value type Replace the ``Workflow._output_executors`` list and the ``RunnerContext.should_label_as_intermediate`` Protocol method with a single immutable ``OutputDesignation`` value type owned by ``Workflow``. Thread the designation as a parameter through the existing call chain (Runner -> EdgeRunner -> Executor -> WorkflowContext) so ``yield_output`` consults the threaded snapshot directly rather than calling back into the runner context. Removes the ``InProcRunnerContext._workflow`` back-reference and the ``WorkflowBuilder.build()`` assignment that wired it up. Adds the public predicate ``Workflow.is_terminal_executor(executor_id)`` for external observers; ``OutputDesignation`` itself stays package-internal. Key decisions - ``OutputDesignation.designated`` is ``frozenset[str] | None`` -- ``None`` preserves legacy "every yield is type='output'" behavior, any frozenset (including empty) opts into strict mode. The ``DeprecationWarning`` for legacy mode at build time is unchanged. - ``output_designation`` is an optional parameter on ``Runner``, ``EdgeRunner.send_message``, ``EdgeRunner._execute_on_target``, ``Executor.execute``, ``Executor._create_context_for_handler``, and ``WorkflowContext.__init__``. Each defaults to legacy ``OutputDesignation()`` so direct callers (Azure Functions ``CapturingRunnerContext``, ``test_runner`` recording fixtures) keep working without ceremony. - The workflow-level filter in ``_run_core`` reads ``self._output_designation`` live, preserving today's semantics where mutating the designation after build still affects subsequent runs (used by two existing tests). - ``Workflow.to_dict()`` continues to emit ``"output_executors": list[str] | None`` (sorted from the frozenset). Checkpoint format unchanged. Files changed - _workflow.py: add ``OutputDesignation`` dataclass; replace ``_output_executors`` with ``_output_designation``; add ``is_terminal_executor``; delete ``_should_yield_output_event``. - _runner_context.py: drop ``should_label_as_intermediate`` Protocol method and ``InProcRunnerContext`` impl; drop ``_workflow`` back-reference. - _workflow_builder.py: remove ``context._workflow = workflow`` assignment. - _runner.py, _edge_runner.py, _executor.py, _workflow_context.py: thread ``output_designation`` parameter through the call chain. - tests/workflow/test_output_designation.py (new): three-state coverage of the value type plus the public predicate delegation. - tests/workflow/test_workflow_builder.py, test_validation.py, test_workflow.py, test_runner.py and orchestrations/tests/test_orchestration_intermediate_vs_terminal.py: switch probes from ``_output_executors`` set checks to ``get_output_executors`` / ``is_terminal_executor``; update two post-build mutation tests to set ``_output_designation`` instead. Verification - core/tests/workflow/, orchestrations/tests/, azurefunctions/tests/: 1119 passed, 42 skipped, 2 xfailed. - ``uv run poe lint``: clean. - ``uv run poe typing``: only the pre-existing ``_AGENT_FORWARDED_EVENT_TYPES`` pyright warning from 394bcd607 remains. Notes for next iteration - The builder's own ``_output_executors`` attribute (``list[Executor | SupportsAgentRun]``) is intentionally untouched; the issue scoped the rename to the workflow attribute. - Adjacent review candidates (twin ``WorkflowAgent`` translators, ``_AGENT_FORWARDED_EVENT_TYPES`` kind classifier, ``_event_origin_context`` ContextVar removal, ``WorkflowEvent`` ADT split, legacy-mode removal) remain out of scope. * Add explicit workflow output designation Key decisions - Extend the internal OutputDesignation value type from terminal-only membership to output/intermediate/hidden classification. Legacy mode remains outputs=None, so workflows built without output_executors or intermediate_executors still label every yield_output as type='output'. - WorkflowBuilder now accepts intermediate_executors. Providing either designation enters explicit mode; output executors emit output, intermediate executors emit intermediate, and unlisted yield_output payloads are hidden from caller-facing events while remaining in executor_completed data. - Empty explicit designation, duplicate entries, overlaps, unknown executors, and designated executors without workflow output annotations fail build validation. Existing orchestration builders pass intermediate-capable participants through intermediate_executors to preserve current intermediate_outputs behavior until participant-oriented designation lands. Files changed - packages/core/agent_framework/_workflows/_workflow.py, _workflow_builder.py, _workflow_context.py, _validation.py, _events.py - packages/core/tests/workflow/test_output_designation.py, test_output_executors_contract.py, test_strict_mode_event_labeling.py, test_validation.py, test_workflow.py, test_workflow_agent_intermediate.py - packages/orchestrations/agent_framework_orchestrations/_sequential.py, _concurrent.py, _group_chat.py, _magentic.py - packages/core/AGENTS.md Verification - uv run pytest packages/core/tests/workflow packages/orchestrations/tests packages/devui/tests/devui/test_mapper.py -q - uv run pytest packages/azurefunctions/tests -q - uv run poe lint - uv run poe typing fails only on pre-existing packages/core/agent_framework/_workflows/_agent.py _AGENT_FORWARDED_EVENT_TYPES private-use pyright error. Notes for next iteration - issues/03-core-workflow-explicit-designation.md was moved to issues/done but issues/ remains untracked and intentionally excluded from this commit. - Slice 4 should tighten workflow.as_agent() mapping for hidden emissions and streaming-only update payloads; Slice 5 should replace orchestration intermediate_outputs with participant-oriented designation. * Tighten workflow-as-agent output mapping Key decisions - Treat AgentResponseUpdate as a streaming-only payload across the workflow.as_agent() adapter, so non-streaming agent runs now reject both terminal output and intermediate workflow events carrying updates. - Keep streaming classification behavior explicit: terminal update payloads remain normal text content, while intermediate update payloads are rewritten to text_reasoning content. - Add explicit-mode coverage proving hidden yield_output emissions do not appear in non-streaming AgentResponse messages or streaming AgentResponseUpdate chunks. Files changed - packages/core/agent_framework/_workflows/_agent.py - packages/core/tests/workflow/test_workflow_agent_intermediate.py Verification - uv run pytest packages/core/tests/workflow/test_workflow_agent_intermediate.py -q - uv run pytest packages/core/tests/workflow/test_workflow_agent.py packages/core/tests/workflow/test_workflow_agent_intermediate.py -q - uv run pytest packages/core/tests/workflow packages/orchestrations/tests packages/devui/tests/devui/test_mapper.py -q - uv run poe lint - uv run poe typing fails only on the pre-existing packages/core/agent_framework/_workflows/_agent.py _AGENT_FORWARDED_EVENT_TYPES private-use pyright error. Blockers or notes for next iteration - issues/04-workflow-as-agent-output-mapping.md was moved to issues/done/ but issues/ remains untracked and intentionally excluded from this commit. - Slice 5 should replace orchestration intermediate_outputs with participant-oriented designation. * Add orchestration participant output designation Key decisions - Replace orchestration intermediate_outputs with participant-oriented output_participants and intermediate_participants across Sequential, Concurrent, GroupChat, Magentic, and Handoff builders. - Keep synthetic final executors terminal by default for Concurrent, GroupChat, and Magentic; keep Sequential's final participant terminal by default; keep Handoff participants terminal by default. - Centralize participant designation validation for empty explicit designation, duplicates, overlaps, and unknown participants, then map validated participants to workflow output/intermediate executors. Files changed - packages/orchestrations/agent_framework_orchestrations/_participant_designation.py - packages/orchestrations/agent_framework_orchestrations/_sequential.py - packages/orchestrations/agent_framework_orchestrations/_concurrent.py - packages/orchestrations/agent_framework_orchestrations/_group_chat.py - packages/orchestrations/agent_framework_orchestrations/_magentic.py - packages/orchestrations/agent_framework_orchestrations/_handoff.py - packages/orchestrations/tests/test_orchestration_intermediate_vs_terminal.py - packages/orchestrations/tests/test_magentic.py Blockers or notes for next iteration - issues/05-orchestration-participant-designation.md was moved to issues/done/ but issues/ remains untracked and intentionally excluded from this commit. - Slice 7 should migrate samples and docs away from intermediate_outputs to the new participant designation API. - uv run poe typing still fails only on the pre-existing packages/core/agent_framework/_workflows/_agent.py _AGENT_FORWARDED_EVENT_TYPES private-use pyright error. * Migrate samples to explicit output designation Key decisions - Replace sample usage of the removed orchestration intermediate_outputs boolean with participant-oriented intermediate_participants designation. - Update raw workflow guidance to show output_executors together with intermediate_executors, and document that unlisted yields are hidden in explicit designation mode. - Keep orchestration final outputs terminal while streaming designated participant responses as intermediate progress, including workflow.as_agent() samples where intermediates map to text_reasoning content. - Refresh workflow and orchestration README guidance plus the changelog reference so public docs no longer point users at intermediate_outputs. Files changed - CHANGELOG.md - packages/orchestrations/README.md - samples/README.md - samples/03-workflows/README.md - samples/03-workflows/control-flow/intermediate_vs_terminal_outputs.py - samples/03-workflows/orchestrations/README.md - samples/03-workflows/orchestrations/group_chat_agent_manager.py - samples/03-workflows/orchestrations/group_chat_philosophical_debate.py - samples/03-workflows/orchestrations/group_chat_simple_selector.py - samples/03-workflows/orchestrations/magentic.py - samples/03-workflows/orchestrations/magentic_human_plan_review.py - samples/03-workflows/orchestrations/sequential_chain_only_agent_responses.py - samples/03-workflows/agents/group_chat_workflow_as_agent.py - samples/03-workflows/agents/magentic_workflow_as_agent.py - samples/03-workflows/agents/sequential_workflow_as_agent.py - samples/semantic-kernel-migration/orchestrations/group_chat.py - samples/semantic-kernel-migration/orchestrations/magentic.py Blockers or notes for next iteration - issues/07-samples-and-docs-explicit-output-designation.md was moved to issues/done/ but issues/ remains untracked and intentionally excluded from this commit. - issues/06-devui-intermediate-event-rendering.md remains present and appears already satisfied by existing DevUI mapper/tests from the prior implementation slice. - PRD-explicit-workflow-output-designation.md remains untracked and intentionally excluded from this commit. * Render DevUI intermediate workflow outputs Key decisions - Preserve workflow output designation metadata on visible DevUI output messages and text deltas so intermediate/data emissions remain distinguishable from terminal output. - Render intermediate workflow message items in the execution timeline using executor metadata, while excluding them from the final workflow result aggregation. - Keep terminal output message rendering unchanged and retain legacy data events on the intermediate compatibility path. Files changed - packages/devui/agent_framework_devui/_mapper.py - packages/devui/frontend/src/components/features/workflow/execution-timeline.tsx - packages/devui/frontend/src/components/features/workflow/workflow-view.tsx - packages/devui/frontend/src/types/openai.ts - packages/devui/tests/devui/test_mapper.py Blockers or notes for next iteration - issues/06-devui-intermediate-event-rendering.md was moved to issues/done/ but issues/ remains untracked and intentionally excluded from this commit. - PRD-explicit-workflow-output-designation.md remains untracked and intentionally excluded from this commit. - uv run poe typing still fails only on the pre-existing packages/core/agent_framework/_workflows/_agent.py _AGENT_FORWARDED_EVENT_TYPES private-use pyright error. * Fix mypy * Clarify orchestration participant output config * Rename participant output kwargs for clarity output_participants -> final_output_from, intermediate_participants -> intermediate_output_from. The old names read like categories of participant; the new names make it clear the kwarg designates which participants' outputs surface as final vs. intermediate events. * Rename core workflow output kwargs with deprecation shim Adds final_output_from / intermediate_output_from as canonical kwargs on Workflow and WorkflowBuilder. Old output_executors / intermediate_executors kwargs continue to work but emit DeprecationWarning via a shared coalesce helper that also rejects supplying both. Wire-format keys in to_dict() stay as output_executors / intermediate_executors so checkpoint compatibility is preserved. Internal call sites in orchestrations and samples updated to the new names so users following sample code learn the canonical vocabulary; legacy callers still work with a one-shot warning. * Suppress pyright reportPrivateUsage on cross-module sentinel import * Update docstrings * Propagate sub-workflow intermediate outputs, fix handoff/sequential intermediate-only designation, and shore up tests, sample, and docstrings around the intermediate output contract. * Add canonical workflow output_from selection Key decisions:\n- Make output_from the canonical workflow-output allow-list and keep output_executors/final_output_from as deprecated compatibility aliases.\n- Treat empty output_from/intermediate_output_from lists as explicit selections and keep validation responsible for empty, duplicate, overlap, and unknown selections.\n- Remove the branch-only public intermediate_executors WorkflowBuilder kwarg while preserving legacy wire keys in to_dict().\n\nFiles changed:\n- packages/core/agent_framework/_workflows/_workflow.py\n- packages/core/agent_framework/_workflows/_workflow_builder.py\n- packages/core/agent_framework/_workflows/_workflow_context.py\n- packages/core/agent_framework/_workflows/_agent.py\n- packages/core/agent_framework/_workflows/_agent_executor.py\n- packages/core/tests/workflow/* output-selection coverage updates\n- packages/core/AGENTS.md\n- issues/done/001-canonical-list-based-output-selection.md\n\nBlockers/notes:\n- Orchestration builders still pass final_output_from internally; follow-up issue 004 should migrate them to output_from.\n- Legacy omitted-selection behavior and explicit all/all_other literals are left for issues 002 and 003. * Add explicit all workflow output selection Key decisions: - Treat output_from='all' as an explicit workflow-output selection sentinel and expand it at build time to executors with declared workflow output types. - Keep omitted output selections in legacy all-output mode with a deprecation warning that names output_from and intermediate_output_from and points to output_from='all'. - Reject intermediate_output_from='all' at construction because the all-output literal is output-only for this issue. Files changed: - packages/core/agent_framework/_workflows/_workflow_builder.py - packages/core/tests/workflow/test_output_executors_contract.py - issues/done/002-explicit-all-output-and-legacy-migration.md Blockers/notes: - all_other intermediate-output selection remains for issue 003. - Workflow-as-agent/orchestration parity remains for issue 004. * Add all-other intermediate output selection Key decisions: - Treat intermediate_output_from='all_other' as an explicit intermediate-output selection sentinel and expand it at build time after the workflow graph is complete. - Expand all_other to output-capable executors not selected by output_from; omitted or empty output_from selects no workflow outputs, while output_from='all' leaves an empty intermediate selection. - Keep output_from='all_other' invalid so all_other remains intermediate-output-only and runtime classification still receives concrete executor-id sets. Files changed: - packages/core/agent_framework/_workflows/_workflow_builder.py - packages/core/tests/workflow/test_output_executors_contract.py - issues/done/003-all-other-intermediate-output-selection.md Blockers/notes: - Workflow-as-agent and orchestration parity remains for issue 004. - Full documentation updates remain for issue 005. * Add orchestration output selection parity Key decisions: - Expose output_from on sequential, concurrent, group chat, handoff, and magentic builders while keeping final_output_from as a deprecated compatibility alias. - Resolve orchestration participant selections through the same explicit rules as workflows: output_from='all', intermediate_output_from='all_other', hidden unselected participant payloads, and overlap/duplicate/unknown/invalid-literal validation. - Continue preserving documented orchestration defaults by always designating each pattern's terminal internal executor where applicable. Files changed: - packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py - packages/orchestrations/agent_framework_orchestrations/_sequential.py - packages/orchestrations/agent_framework_orchestrations/_concurrent.py - packages/orchestrations/agent_framework_orchestrations/_group_chat.py - packages/orchestrations/agent_framework_orchestrations/_handoff.py - packages/orchestrations/agent_framework_orchestrations/_magentic.py - packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py - packages/orchestrations/tests/test_orchestration_intermediate_vs_terminal.py - issues/done/004-workflow-as-agent-and-orchestration-parity.md Blockers/notes: - Full documentation and sample migration wording remains for issue 005. - Existing tests that intentionally use final_output_from now emit the new deprecation warning. * Document workflow output selection contract Key decisions: - Use Workflow Output and Intermediate Output as the developer-facing terms for selected caller-facing emissions. - Document output_from and intermediate_output_from as the canonical API, with output_from as an allow-list and unselected payloads hidden unless explicitly selected as intermediate. - Add scenario and invalid-selection tables for workflow and orchestration docs, including legacy omission warnings, output_from='all', intermediate_output_from='all_other', list selections, invalid literals, overlap, duplicates, unknown selections, and empty explicit selections. - Migrate samples away from final_output_from and output_executors except where compatibility aliases are explicitly documented. Files changed: - packages/core/AGENTS.md - packages/orchestrations/README.md - packages/orchestrations/agent_framework_orchestrations/_handoff.py - packages/orchestrations/agent_framework_orchestrations/_sequential.py - samples/03-workflows/README.md - samples/03-workflows/control-flow/intermediate_vs_terminal_outputs.py - samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py - samples/03-workflows/orchestrations/README.md - samples/04-hosting/foundry-hosted-agents/responses/05_workflows/main.py - scripts/sample_validation/create_dynamic_workflow_executor.py - issues/done/005-document-output-selection-contract.md Blockers/notes: - Direct full Ruff on scripts/sample_validation/create_dynamic_workflow_executor.py still reports pre-existing docstring/print/line-length issues outside this docs migration; syntax-focused checks for changed files pass. - No remaining AFK issue files are present under issues/. * Latest updates * Typing fixes * Cleanup --- python/CHANGELOG.md | 2 +- .../agent_framework_azurefunctions/_app.py | 13 + .../_context.py | 10 + .../azurefunctions/tests/test_func_utils.py | 10 +- python/packages/core/AGENTS.md | 9 +- .../core/agent_framework/_workflows/_agent.py | 47 +- .../_workflows/_agent_executor.py | 4 +- .../_workflows/_edge_runner.py | 40 +- .../agent_framework/_workflows/_events.py | 52 +- .../agent_framework/_workflows/_functional.py | 3 +- .../_workflows/_runner_context.py | 22 +- .../agent_framework/_workflows/_validation.py | 32 +- .../agent_framework/_workflows/_workflow.py | 202 ++++- .../_workflows/_workflow_builder.py | 230 +++++- .../_workflows/_workflow_context.py | 30 +- .../_workflows/_workflow_executor.py | 18 +- .../test_agent_executor_tool_calls.py | 16 +- .../workflow/test_agent_run_event_typing.py | 6 +- .../tests/workflow/test_full_conversation.py | 10 +- .../workflow/test_functional_workflow.py | 8 +- .../tests/workflow/test_output_designation.py | 137 ++++ .../test_output_executors_contract.py | 287 +++++++ .../core/tests/workflow/test_runner.py | 16 +- .../core/tests/workflow/test_serialization.py | 45 ++ .../test_strict_mode_event_labeling.py | 118 +++ .../core/tests/workflow/test_sub_workflow.py | 72 ++ .../core/tests/workflow/test_validation.py | 83 +- .../core/tests/workflow/test_workflow.py | 30 +- .../tests/workflow/test_workflow_agent.py | 2 +- .../test_workflow_agent_intermediate.py | 353 +++++++++ .../tests/workflow/test_workflow_builder.py | 48 +- .../tests/workflow/test_workflow_context.py | 26 + .../workflow/test_workflow_event_factories.py | 34 + .../tests/workflow/test_workflow_kwargs.py | 2 +- .../devui/agent_framework_devui/_mapper.py | 78 +- .../features/workflow/execution-timeline.tsx | 45 +- .../features/workflow/workflow-view.tsx | 42 +- .../devui/frontend/src/types/openai.ts | 2 + .../packages/devui/tests/devui/test_mapper.py | 113 ++- .../foundry/tests/test_foundry_evals.py | 12 +- python/packages/orchestrations/README.md | 45 ++ .../_concurrent.py | 36 +- .../_group_chat.py | 38 +- .../_handoff.py | 35 +- .../_magentic.py | 38 +- .../_orchestration_request_info.py | 8 +- .../_participant_output_config.py | 166 ++++ .../_sequential.py | 41 +- .../orchestrations/tests/test_magentic.py | 9 +- ..._orchestration_intermediate_vs_terminal.py | 749 ++++++++++++++++++ python/samples/03-workflows/README.md | 40 +- .../agents/group_chat_workflow_as_agent.py | 6 +- .../agents/magentic_workflow_as_agent.py | 6 +- .../agents/sequential_workflow_as_agent.py | 6 +- .../intermediate_vs_terminal_outputs.py | 156 ++++ .../agents_with_approval_requests.py | 2 +- .../03-workflows/orchestrations/README.md | 37 +- .../group_chat_agent_manager.py | 9 +- .../group_chat_philosophical_debate.py | 11 +- .../group_chat_simple_selector.py | 9 +- .../03-workflows/orchestrations/magentic.py | 9 +- .../magentic_human_plan_review.py | 11 +- .../sequential_chain_only_agent_responses.py | 4 +- .../responses/05_workflows/main.py | 6 +- python/samples/README.md | 2 +- .../orchestrations/group_chat.py | 4 +- .../orchestrations/magentic.py | 4 +- .../create_dynamic_workflow_executor.py | 9 +- 68 files changed, 3480 insertions(+), 325 deletions(-) create mode 100644 python/packages/core/tests/workflow/test_output_designation.py create mode 100644 python/packages/core/tests/workflow/test_output_executors_contract.py create mode 100644 python/packages/core/tests/workflow/test_strict_mode_event_labeling.py create mode 100644 python/packages/core/tests/workflow/test_workflow_agent_intermediate.py create mode 100644 python/packages/core/tests/workflow/test_workflow_event_factories.py create mode 100644 python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py create mode 100644 python/packages/orchestrations/tests/test_orchestration_intermediate_vs_terminal.py create mode 100644 python/samples/03-workflows/control-flow/intermediate_vs_terminal_outputs.py diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index e2adf2762e..e800652483 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -67,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **agent-framework-foundry-hosting**: Add hosted Durable Workflow support — propagate full conversation history to workflow agents and wire `Workflow.as_agent()` end-to-end via the foundry hosting layer ([#5531](https://github.com/microsoft/agent-framework/pull/5531)) ### Changed -- **agent-framework-orchestrations**: [BREAKING] Standardize orchestration terminal outputs as `AgentResponse` so `Workflow.as_agent()` returns the final answer only; aligns sequential-approval (`with_request_info`) and concurrent (`intermediate_outputs=True`) flows on the same output contract ([#5301](https://github.com/microsoft/agent-framework/pull/5301)) +- **agent-framework-orchestrations**: [BREAKING] Standardize orchestration terminal outputs as `AgentResponse` so `Workflow.as_agent()` returns the final answer only; aligns sequential-approval (`with_request_info`) and concurrent participant output designation flows on the same output contract ([#5301](https://github.com/microsoft/agent-framework/pull/5301)) - **agent-framework-core**, **agent-framework-declarative**: Preserve `Workflow.run()` shared state across calls so multi-turn `WorkflowAgent` invocations retain context, accept `list[Message]` input in the declarative start executor, and coerce `Enum` values when serializing PowerFx symbols ([#5531](https://github.com/microsoft/agent-framework/pull/5531)) - **dependencies**: Update workspace package dependencies and preserve `mcp[ws]` / `uvicorn[standard]` extras through override-dependencies in `/python` ([#5555](https://github.com/microsoft/agent-framework/pull/5555)) diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py index e1164154a5..c25c2461ce 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py @@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Any, TypeVar, cast import azure.durable_functions as df import azure.functions as func from agent_framework import AgentExecutor, SupportsAgentRun, Workflow, WorkflowEvent +from agent_framework._workflows._runner_context import YieldOutputEventType from agent_framework_durabletask import ( DEFAULT_MAX_POLL_RETRIES, DEFAULT_POLL_INTERVAL_SECONDS, @@ -307,6 +308,18 @@ class AgentFunctionApp(DFAppBase): async def run() -> dict[str, Any]: # Create runner context and shared state runner_context = CapturingRunnerContext() + workflow = self.workflow + + def classify_yielded_output(executor_id: str) -> YieldOutputEventType | None: + if workflow is None: + return "output" + if workflow.is_terminal_executor(executor_id): + return "output" + if workflow.is_intermediate_executor(executor_id): + return "intermediate" + return None + + runner_context.set_yield_output_classifier(classify_yielded_output) shared_state = State() # Deserialize shared state values to reconstruct dataclasses/Pydantic models diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py index a45dcf81fc..4912fe4cc9 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_context.py @@ -19,6 +19,7 @@ from agent_framework import ( WorkflowEvent, WorkflowMessage, ) +from agent_framework._workflows._runner_context import YieldOutputClassifier, YieldOutputEventType from agent_framework._workflows._state import State @@ -41,6 +42,7 @@ class CapturingRunnerContext(RunnerContext): self._pending_request_info_events: dict[str, WorkflowEvent[Any]] = {} self._workflow_id: str | None = None self._streaming: bool = False + self._yield_output_classifier: YieldOutputClassifier = lambda _executor_id: "output" # region Messaging @@ -144,6 +146,14 @@ class CapturingRunnerContext(RunnerContext): """Check if streaming mode is enabled (always False in activity context).""" return self._streaming + def set_yield_output_classifier(self, classifier: YieldOutputClassifier) -> None: + """Set the classifier used by WorkflowContext.yield_output().""" + self._yield_output_classifier = classifier + + def classify_yielded_output(self, executor_id: str) -> YieldOutputEventType | None: + """Classify an executor's yield_output payload as output, intermediate, or hidden.""" + return self._yield_output_classifier(executor_id) + # endregion Workflow Configuration # region Request Info Events diff --git a/python/packages/azurefunctions/tests/test_func_utils.py b/python/packages/azurefunctions/tests/test_func_utils.py index 6110c0f895..30841eaec0 100644 --- a/python/packages/azurefunctions/tests/test_func_utils.py +++ b/python/packages/azurefunctions/tests/test_func_utils.py @@ -107,7 +107,7 @@ class TestCapturingRunnerContext: @pytest.mark.asyncio async def test_add_event_queues_event(self, context: CapturingRunnerContext) -> None: """Test that add_event queues events correctly.""" - event = WorkflowEvent.output(executor_id="exec_1", data="output") + event = WorkflowEvent("output", executor_id="exec_1", data="output") await context.add_event(event) @@ -120,7 +120,7 @@ class TestCapturingRunnerContext: @pytest.mark.asyncio async def test_drain_events_clears_queue(self, context: CapturingRunnerContext) -> None: """Test that drain_events clears the event queue.""" - await context.add_event(WorkflowEvent.output(executor_id="e", data="test")) + await context.add_event(WorkflowEvent("output", executor_id="e", data="test")) await context.drain_events() # First drain events = await context.drain_events() # Second drain @@ -132,14 +132,14 @@ class TestCapturingRunnerContext: """Test has_events returns correct boolean.""" assert await context.has_events() is False - await context.add_event(WorkflowEvent.output(executor_id="e", data="test")) + await context.add_event(WorkflowEvent("output", executor_id="e", data="test")) assert await context.has_events() is True @pytest.mark.asyncio async def test_next_event_waits_for_event(self, context: CapturingRunnerContext) -> None: """Test that next_event returns queued events.""" - event = WorkflowEvent.output(executor_id="e", data="waited") + event = WorkflowEvent("output", executor_id="e", data="waited") await context.add_event(event) result = await context.next_event() @@ -171,7 +171,7 @@ class TestCapturingRunnerContext: async def test_reset_for_new_run_clears_state(self, context: CapturingRunnerContext) -> None: """Test that reset_for_new_run clears all state.""" await context.send_message(WorkflowMessage(data="test", target_id="t", source_id="s")) - await context.add_event(WorkflowEvent.output(executor_id="e", data="event")) + await context.add_event(WorkflowEvent("output", executor_id="e", data="event")) context.set_streaming(True) context.reset_for_new_run() diff --git a/python/packages/core/AGENTS.md b/python/packages/core/AGENTS.md index edd4eaa158..ed47f363a2 100644 --- a/python/packages/core/AGENTS.md +++ b/python/packages/core/AGENTS.md @@ -79,7 +79,14 @@ agent_framework/ ### Workflows (`_workflows/`) - **`Workflow`** - Graph-based workflow definition -- **`WorkflowBuilder`** - Fluent API for building workflows +- **`WorkflowBuilder`** - Fluent API for building workflows, including explicit + `output_from` / `intermediate_output_from` selection for caller-facing emissions. `output_from` + is an allow-list for **Workflow Output**; unselected executor payloads are hidden unless + `intermediate_output_from` selects them as **Intermediate Output**. Use `output_from="all"` for + explicit all-output behavior and `intermediate_output_from="all_other"` for visible progress from + every output-capable executor not selected by `output_from`. +- **`WorkflowRunResult`** - Non-streaming workflow result with Workflow Output `get_outputs()` + and Intermediate Output `get_intermediate_outputs()` accessors - **Orchestrators**: `SequentialOrchestrator`, `ConcurrentOrchestrator`, `GroupChatOrchestrator`, `MagenticOrchestrator`, `HandoffOrchestrator` ## Built-in Providers diff --git a/python/packages/core/agent_framework/_workflows/_agent.py b/python/packages/core/agent_framework/_workflows/_agent.py index f8a85a261c..2d9b37e1f5 100644 --- a/python/packages/core/agent_framework/_workflows/_agent.py +++ b/python/packages/core/agent_framework/_workflows/_agent.py @@ -32,6 +32,7 @@ from .._types import ( from ..exceptions import AgentInvalidRequestException, AgentInvalidResponseException from ._checkpoint import CheckpointStorage from ._events import ( + AGENT_FORWARDED_EVENT_TYPES, WorkflowEvent, ) from ._message_utils import normalize_messages_input @@ -104,7 +105,7 @@ class WorkflowAgent(BaseAgent): Note: Only output events (type='output') and request_info events (type='request_info') from the workflow are considered and converted to agent responses of the WorkflowAgent. - Other workflow events are ignored. Use `with_output_from` in WorkflowBuilder to control + Other workflow events are ignored. Use `output_from` in WorkflowBuilder to control which executors' outputs are surfaced as agent responses. """ if id is None: @@ -300,7 +301,7 @@ class WorkflowAgent(BaseAgent): function_invocation_kwargs=function_invocation_kwargs, client_kwargs=client_kwargs, ): - if event.type == "output" or event.type == "request_info": + if event.type in AGENT_FORWARDED_EVENT_TYPES: output_events.append(event) result = self._convert_workflow_events_to_agent_response(response_id, output_events) @@ -514,7 +515,11 @@ class WorkflowAgent(BaseAgent): response_id: str, output_events: list[WorkflowEvent[Any]], ) -> AgentResponse: - """Convert a list of workflow output events to an AgentResponse.""" + """Convert a list of workflow events to an AgentResponse. + + Caller-facing workflow events are forwarded as agent messages. Terminal and + intermediate event payloads keep their original content types. + """ messages: list[Message] = [] raw_representations: list[object] = [] merged_usage: UsageDetails | None = None @@ -535,14 +540,19 @@ class WorkflowAgent(BaseAgent): raw_representations.append(output_event) else: data = output_event.data + # Anything that isn't `output` is intermediate — this branch only sees + # events that already passed the lifecycle filter and weren't request_info. + is_intermediate = output_event.type != "output" if isinstance(data, AgentResponseUpdate): - # We cannot support AgentResponseUpdate in non-streaming mode. This is because the message - # sequence cannot be guaranteed when there are streaming updates in between non-streaming - # responses. + # AgentResponseUpdate is a streaming-only payload. Accepting it + # in non-streaming runs would make message ordering depend on + # partial chunks for both terminal and intermediate events. + event_label = "Intermediate" if is_intermediate else "Output" raise AgentInvalidRequestException( - "Output event with AgentResponseUpdate data cannot be emitted in non-streaming mode. " - "Please ensure executors emit AgentResponse for non-streaming workflows." + f"{event_label} event with AgentResponseUpdate data cannot be emitted " + "in non-streaming mode. Please ensure executors emit AgentResponse " + "for non-streaming workflows." ) if isinstance(data, AgentResponse): @@ -626,16 +636,21 @@ class WorkflowAgent(BaseAgent): ) -> list[AgentResponseUpdate]: """Convert a workflow event to a list of AgentResponseUpdate objects. - Events with type='output' and type='request_info' are processed. - Other workflow events are ignored as they are workflow-internal. + Forwarding rule: - For 'output' events, AgentExecutor yields AgentResponseUpdate for streaming updates - via ctx.yield_output(). This method converts those to agent response updates. - - Returns: - A list of AgentResponseUpdate objects. Empty list if the event is not relevant. + - ``type='output'`` — terminal user-facing emission. Forwarded as-is. + - ``type='intermediate'`` (and the deprecated ``type='data'``) — forwarded + as-is. + - ``type='request_info'`` — request-info translation (unchanged). + - Everything else (lifecycle, diagnostics, executor bookkeeping, + orchestration-internal events like ``group_chat``/``handoff_sent``/ + ``magentic_orchestrator``) is dropped. """ - if event.type == "output": + # TODO(evmattso): https://github.com/microsoft/agent-framework/issues/5885 + if event.type not in AGENT_FORWARDED_EVENT_TYPES: + return [] + + if event.type != "request_info": data = event.data executor_id = event.executor_id diff --git a/python/packages/core/agent_framework/_workflows/_agent_executor.py b/python/packages/core/agent_framework/_workflows/_agent_executor.py index 9b16b1f291..16e4fd3def 100644 --- a/python/packages/core/agent_framework/_workflows/_agent_executor.py +++ b/python/packages/core/agent_framework/_workflows/_agent_executor.py @@ -123,7 +123,7 @@ class AgentExecutor(Executor): - run(stream=True): Emits incremental output events (type='output') as the agent produces tokens - run(): Emits a single output event (type='output') containing the complete response - Use `with_output_from` in WorkflowBuilder to control whether the AgentResponse + Use `output_from` in WorkflowBuilder to control whether the AgentResponse or AgentResponseUpdate objects are yielded as workflow outputs. Messages sent to downstream executors will always be the complete AgentResponse. In @@ -478,7 +478,7 @@ class AgentExecutor(Executor): # Prefer stream finalization when available so result hooks run # (e.g., thread conversation updates). Fall back to reconstructing from updates - # for legacy/custom agents that return a plain async iterable. + # for compatibility/custom agents that return a plain async iterable. # TODO(evmattso): Integrate workflow agent run handling around ResponseStream so # AgentExecutor does not need this conditional stream-finalization branch. maybe_get_final_response = getattr(stream, "get_final_response", None) diff --git a/python/packages/core/agent_framework/_workflows/_edge_runner.py b/python/packages/core/agent_framework/_workflows/_edge_runner.py index 06188e9fb2..586ccd9c3a 100644 --- a/python/packages/core/agent_framework/_workflows/_edge_runner.py +++ b/python/packages/core/agent_framework/_workflows/_edge_runner.py @@ -38,7 +38,12 @@ class EdgeRunner(ABC): self._executors = executors @abstractmethod - async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: + async def send_message( + self, + message: WorkflowMessage, + state: State, + ctx: RunnerContext, + ) -> bool: """Send a message through the edge group. Args: @@ -90,7 +95,12 @@ class SingleEdgeRunner(EdgeRunner): super().__init__(edge_group, executors) self._edge = edge_group.edges[0] - async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: + async def send_message( + self, + message: WorkflowMessage, + state: State, + ctx: RunnerContext, + ) -> bool: """Send a message through the single edge.""" should_execute = False target_id: str | None = None @@ -162,7 +172,12 @@ class FanOutEdgeRunner(EdgeRunner): Callable[[Any, list[str]], list[str]] | None, getattr(edge_group, "selection_func", None) ) - async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: + async def send_message( + self, + message: WorkflowMessage, + state: State, + ctx: RunnerContext, + ) -> bool: """Send a message through all edges in the fan-out edge group.""" deliverable_edges: list[Edge] = [] single_target_edge: Edge | None = None @@ -253,7 +268,11 @@ class FanOutEdgeRunner(EdgeRunner): # Execute outside the span if single_target_edge: await self._execute_on_target( - single_target_edge.target_id, [single_target_edge.source_id], message, state, ctx + single_target_edge.target_id, + [single_target_edge.source_id], + message, + state, + ctx, ) return True @@ -285,7 +304,12 @@ class FanInEdgeRunner(EdgeRunner): # Key is the source executor ID, value is a list of messages self._buffer: dict[str, list[WorkflowMessage]] = defaultdict(list) - async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: + async def send_message( + self, + message: WorkflowMessage, + state: State, + ctx: RunnerContext, + ) -> bool: """Send a message through all edges in the fan-in edge group.""" execution_data: dict[str, Any] | None = None with create_edge_group_processing_span( @@ -362,7 +386,11 @@ class FanInEdgeRunner(EdgeRunner): # Execute outside the span if needed if execution_data: await self._execute_on_target( - execution_data["target_id"], execution_data["source_ids"], execution_data["message"], state, ctx + execution_data["target_id"], + execution_data["source_ids"], + execution_data["message"], + state, + ctx, ) return True diff --git a/python/packages/core/agent_framework/_workflows/_events.py b/python/packages/core/agent_framework/_workflows/_events.py index 4b8238268c..aa1a69954f 100644 --- a/python/packages/core/agent_framework/_workflows/_events.py +++ b/python/packages/core/agent_framework/_workflows/_events.py @@ -5,6 +5,7 @@ from __future__ import annotations import builtins import sys import traceback as _traceback +import warnings from collections.abc import Iterator from contextlib import contextmanager from contextvars import ContextVar @@ -106,8 +107,9 @@ WorkflowEventType = Literal[ "status", # Workflow state changed (use .state) "failed", # Workflow terminated with error (use .details) # Data events - "output", # Executor yielded final output (use .executor_id, .data) - "data", # Executor emitted data during execution (use .executor_id, .data) + "output", # Executor yielded final terminal output (use .executor_id, .data) + "intermediate", # Executor emitted intermediate (non-terminal) output (use .executor_id, .data) + "data", # DEPRECATED — compatibility alias for intermediate emissions; use type='intermediate' instead. # Request events (human-in-the-loop) "request_info", # Executor requests external info (use .request_id, .source_executor_id) # Diagnostic events (warnings/errors from user code) @@ -128,21 +130,34 @@ WorkflowEventType = Literal[ ] +# Event types forwarded across the ``workflow.as_agent()`` boundary. Anything not +# in this set — lifecycle events, diagnostics, executor bookkeeping, and +# orchestration-internal events (``group_chat``, ``handoff_sent``, +# ``magentic_orchestrator``) — stays inside the workflow and is not surfaced to +# agent callers. Internal to the ``_workflows`` package. +AGENT_FORWARDED_EVENT_TYPES: frozenset[str] = frozenset({ + "output", + "intermediate", + "data", # deprecated alias for intermediate; retained for backward compat + "request_info", +}) + + class WorkflowEvent(Generic[DataT]): """Unified event for all workflow emissions. This single generic class handles all workflow events through a `type` discriminator, following the same pattern as the `Content` class. - Use factory methods for convenient construction: + Use factory methods for convenient construction of lifecycle, diagnostic, request, + and executor bookkeeping events. Workflow ``output`` and ``intermediate`` events + are emitted by ``ctx.yield_output(...)`` based on workflow output selection. - `WorkflowEvent.started()` - workflow run began - `WorkflowEvent.status(state)` - workflow state changed - `WorkflowEvent.failed(details)` - workflow terminated with error - `WorkflowEvent.warning(message)` - warning from user code - `WorkflowEvent.error(exception)` - error from user code - - `WorkflowEvent.output(executor_id, data)` - executor yielded final output - - `WorkflowEvent.data(executor_id, data)` - executor emitted data (e.g., AgentResponse) - `WorkflowEvent.request_info(...)` - executor requests external info - `WorkflowEvent.superstep_started(iteration)` - superstep began - `WorkflowEvent.superstep_completed(iteration)` - superstep ended @@ -158,14 +173,13 @@ class WorkflowEvent(Generic[DataT]): Examples: .. code-block:: python - # Create events via factory methods + # Create lifecycle events via factory methods started = WorkflowEvent.started() status = WorkflowEvent.status(WorkflowRunState.IN_PROGRESS) - output = WorkflowEvent.output("agent1", result_data) - # Emit typed data from executor - event: WorkflowEvent[AgentResponse] = WorkflowEvent.data("agent1", response) - data: AgentResponse = event.data # Type-safe access + # Type-safe access to event data + event: WorkflowEvent[AgentResponse] = WorkflowEvent("data", executor_id="agent1", data=response) + data: AgentResponse = event.data # Check event type if event.type == "status": @@ -263,18 +277,20 @@ class WorkflowEvent(Generic[DataT]): """Create an 'error' event from user code.""" return WorkflowEvent("error", data=exception) - @classmethod - def output(cls, executor_id: str, data: DataT) -> WorkflowEvent[DataT]: - """Create an 'output' event when an executor yields final output.""" - return cls("output", executor_id=executor_id, data=data) - @classmethod def emit(cls, executor_id: str, data: DataT) -> WorkflowEvent[DataT]: - """Create a 'data' event when an executor emits data during execution. + """Create a 'data' event (deprecated alias for intermediate emissions). - This is the primary method for executors to emit typed data - (e.g., AgentResponse, AgentResponseUpdate, custom data). + .. deprecated:: + Use ``ctx.yield_output(...)`` and configure ``intermediate_output_from`` instead. + Will be removed in a future major release along with the ``type='data'`` event variant. """ + warnings.warn( + "WorkflowEvent.emit() / type='data' are deprecated; use ctx.yield_output() from an " + "intermediate-designated executor. Will be removed in a future major release.", + DeprecationWarning, + stacklevel=2, + ) return cls("data", executor_id=executor_id, data=data) @classmethod diff --git a/python/packages/core/agent_framework/_workflows/_functional.py b/python/packages/core/agent_framework/_workflows/_functional.py index 159d75e137..5746c2161c 100644 --- a/python/packages/core/agent_framework/_workflows/_functional.py +++ b/python/packages/core/agent_framework/_workflows/_functional.py @@ -982,7 +982,8 @@ class FunctionalWorkflow: # Emit the return value as the workflow output. if return_value is not None: - await ctx.add_event(WorkflowEvent.output(self.name, return_value)) + with _framework_event_origin(): + await ctx.add_event(WorkflowEvent("output", executor_id=self.name, data=return_value)) # Persist step cache for response-only replay self._last_step_cache = dict(ctx._step_cache) diff --git a/python/packages/core/agent_framework/_workflows/_runner_context.py b/python/packages/core/agent_framework/_workflows/_runner_context.py index e3711ea96f..2e4901f411 100644 --- a/python/packages/core/agent_framework/_workflows/_runner_context.py +++ b/python/packages/core/agent_framework/_workflows/_runner_context.py @@ -4,10 +4,11 @@ from __future__ import annotations import asyncio import logging +from collections.abc import Callable from copy import copy from dataclasses import dataclass from enum import Enum -from typing import Any, Protocol, TypeVar, runtime_checkable +from typing import Any, Literal, Protocol, TypeVar, runtime_checkable from ._checkpoint import CheckpointID, CheckpointStorage, WorkflowCheckpoint from ._const import INTERNAL_SOURCE_ID @@ -18,6 +19,8 @@ from ._typing_utils import is_instance_of logger = logging.getLogger(__name__) T = TypeVar("T") +YieldOutputEventType = Literal["output", "intermediate"] +YieldOutputClassifier = Callable[[str], YieldOutputEventType | None] class MessageType(Enum): @@ -263,6 +266,14 @@ class RunnerContext(Protocol): """ ... + def set_yield_output_classifier(self, classifier: YieldOutputClassifier) -> None: + """Set the classifier used by WorkflowContext.yield_output().""" + ... + + def classify_yielded_output(self, executor_id: str) -> YieldOutputEventType | None: + """Classify an executor's yield_output payload as output, intermediate, or hidden.""" + ... + class InProcRunnerContext: """In-process execution context for local execution and optional checkpointing.""" @@ -286,6 +297,7 @@ class InProcRunnerContext: # Streaming flag - set by workflow's run(..., stream=True) vs run(..., stream=False) self._streaming: bool = False + self._yield_output_classifier: YieldOutputClassifier = lambda _executor_id: "output" # region Messaging and Events async def send_message(self, message: WorkflowMessage) -> None: @@ -480,3 +492,11 @@ class InProcRunnerContext: A dictionary mapping request IDs to their corresponding WorkflowEvent (type='request_info'). """ return dict(self._pending_request_info_events) + + def set_yield_output_classifier(self, classifier: YieldOutputClassifier) -> None: + """Set the classifier used by WorkflowContext.yield_output().""" + self._yield_output_classifier = classifier + + def classify_yielded_output(self, executor_id: str) -> YieldOutputEventType | None: + """Classify an executor's yield_output payload as output, intermediate, or hidden.""" + return self._yield_output_classifier(executor_id) diff --git a/python/packages/core/agent_framework/_workflows/_validation.py b/python/packages/core/agent_framework/_workflows/_validation.py index 990668f340..d8aa4c80f5 100644 --- a/python/packages/core/agent_framework/_workflows/_validation.py +++ b/python/packages/core/agent_framework/_workflows/_validation.py @@ -104,6 +104,7 @@ class WorkflowGraphValidator: executors: dict[str, Executor], start_executor: Executor, output_executors: list[str], + intermediate_executors: list[str] | None = None, ) -> None: """Validate the entire workflow graph. @@ -112,6 +113,7 @@ class WorkflowGraphValidator: executors: Map of executor IDs to executor instances start_executor: The starting executor output_executors: List of output executor IDs + intermediate_executors: List of intermediate executor IDs Raises: WorkflowValidationError: If any validation fails @@ -158,7 +160,7 @@ class WorkflowGraphValidator: self._validate_graph_connectivity(start_executor.id) self._validate_self_loops() self._validate_dead_ends() - self._output_validation(output_executors) + self._output_validation(output_executors, intermediate_executors or []) def _validate_handler_output_annotations(self) -> None: """Validate that each handler's ctx parameter is annotated with WorkflowContext[T]. @@ -356,8 +358,15 @@ class WorkflowGraphValidator: # region Output Validation - def _output_validation(self, output_executors: list[str]) -> None: - """Validate that output executors exist in the workflow and have the correct workflow context annotations.""" + def _output_validation(self, output_executors: list[str], intermediate_executors: list[str]) -> None: + """Validate that designated executors exist and have workflow output annotations.""" + overlap = sorted(set(output_executors).intersection(intermediate_executors)) + if overlap: + raise WorkflowValidationError( + f"Executors cannot be both output and intermediate designated: {overlap}", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) + for output_id in output_executors: if output_id not in self._executors: raise WorkflowValidationError( @@ -372,6 +381,20 @@ class WorkflowGraphValidator: validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, ) + for intermediate_id in intermediate_executors: + if intermediate_id not in self._executors: + raise WorkflowValidationError( + f"Intermediate executor '{intermediate_id}' is not present in the workflow graph", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) + + intermediate_executor = self._executors[intermediate_id] + if not intermediate_executor.workflow_output_types: + raise WorkflowValidationError( + f"Intermediate executor '{intermediate_id}' must have output type annotations defined.", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) + # endregion # region Additional Validation Scenarios @@ -415,6 +438,7 @@ def validate_workflow_graph( executors: dict[str, Executor], start_executor: Executor, output_executors: list[str], + intermediate_executors: list[str] | None = None, ) -> None: """Convenience function to validate a workflow graph. @@ -423,6 +447,7 @@ def validate_workflow_graph( executors: Map of executor IDs to executor instances start_executor: The starting executor instance output_executors: List of output executor IDs + intermediate_executors: List of intermediate executor IDs Raises: WorkflowValidationError: If any validation fails @@ -433,4 +458,5 @@ def validate_workflow_graph( executors, start_executor, output_executors, + intermediate_executors, ) diff --git a/python/packages/core/agent_framework/_workflows/_workflow.py b/python/packages/core/agent_framework/_workflows/_workflow.py index 1c67b8b6f8..0493cd015f 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow.py +++ b/python/packages/core/agent_framework/_workflows/_workflow.py @@ -10,7 +10,9 @@ import json import logging import types import uuid +import warnings from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence +from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal, overload from .._sessions import ContextProvider @@ -34,6 +36,7 @@ from ._runner import Runner from ._runner_context import RunnerContext from ._state import State from ._typing_utils import is_instance_of, try_coerce_to_type +from ._validation import ValidationTypeEnum, WorkflowValidationError if TYPE_CHECKING: from ._agent import WorkflowAgent @@ -41,6 +44,60 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +_MISSING: Any = object() + + +def _coalesce_renamed_kwarg(old_name: str, old_value: Any, new_name: str, new_value: Any) -> Any: + """Resolve a renamed keyword argument while keeping the deprecated name working. + + Pass ``_MISSING`` (not ``None``) for the value that was not supplied — ``None`` is + a legitimate user-supplied value for these kwargs. + """ + old_supplied = old_value is not _MISSING + new_supplied = new_value is not _MISSING + if old_supplied and new_supplied: + raise TypeError(f"Cannot pass both `{old_name}` (deprecated) and `{new_name}`; use `{new_name}` only.") + if old_supplied: + warnings.warn( + f"`{old_name}` is deprecated and will be removed in a future version; use `{new_name}` instead.", + DeprecationWarning, + stacklevel=3, + ) + return old_value + if new_supplied: + return new_value + return None + + +def _coalesce_output_from_kwarg( + output_from: Any, + output_executors: Any, +) -> Any: + """Resolve output-selection aliases to canonical ``output_from``.""" + supplied = [ + name + for name, value in ( + ("output_from", output_from), + ("output_executors", output_executors), + ) + if value is not _MISSING + ] + if len(supplied) > 1: + formatted = ", ".join(f"`{name}`" for name in supplied) + raise TypeError(f"Cannot pass multiple workflow output selection parameters ({formatted}); use `output_from`.") + + if output_executors is not _MISSING: + warnings.warn( + "`output_executors` is deprecated and will be removed in a future version; use `output_from` instead.", + DeprecationWarning, + stacklevel=3, + ) + return output_executors + if output_from is not _MISSING: + return output_from + return None + + class WorkflowRunResult(list[WorkflowEvent]): """Container for events generated during non-streaming workflow execution. @@ -73,6 +130,14 @@ class WorkflowRunResult(list[WorkflowEvent]): """ return [event.data for event in self if event.type == "output"] + def get_intermediate_outputs(self) -> list[Any]: + """Get all intermediate outputs from the workflow run result. + + Returns: + A list of intermediate outputs produced by the workflow during its execution. + """ + return [event.data for event in self if event.type == "intermediate"] + def get_request_info_events(self) -> list[WorkflowEvent[Any]]: """Get all request info events from the workflow run result. @@ -102,6 +167,42 @@ class WorkflowRunResult(list[WorkflowEvent]): # region Workflow +@dataclass(frozen=True) +class OutputDesignation: + """Immutable rule for labeling executor yields as terminal, intermediate, or hidden outputs. + + ``outputs`` is ``None`` in omitted-selection compatibility mode (every yield is terminal). In explicit mode, + ``outputs`` and ``intermediates`` are disjoint executor ID sets; unlisted executor + yields are hidden from caller-facing output/intermediate events. + Package-internal value type owned by ``Workflow``; not exported from ``agent_framework``. + """ + + outputs: frozenset[str] | None = field(default=None) + intermediates: frozenset[str] = field(default_factory=lambda: frozenset[str]()) + + def is_terminal(self, executor_id: str) -> bool: + """Return True when ``executor_id``'s yields should be labeled type='output'.""" + if self.outputs is None: + return True + return executor_id in self.outputs + + def is_intermediate(self, executor_id: str) -> bool: + """Return True when ``executor_id``'s yields should be labeled type='intermediate'.""" + if self.outputs is None: + return False + return executor_id in self.intermediates + + def classify(self, executor_id: str) -> Literal["output", "intermediate"] | None: + """Return the workflow event type for this executor's yield, or None when hidden.""" + if self.outputs is None: + return "output" + if executor_id in self.outputs: + return "output" + if executor_id in self.intermediates: + return "intermediate" + return None + + class Workflow(DictConvertible): """A graph-based execution engine that orchestrates connected executors. @@ -182,7 +283,11 @@ class Workflow(DictConvertible): name: str, description: str | None = None, max_iterations: int = DEFAULT_MAX_ITERATIONS, - output_executors: list[str] | None = None, + output_from: list[str] | None = _MISSING, + intermediate_output_from: list[str] | None = _MISSING, + *, + output_executors: list[str] | None = _MISSING, + intermediate_executors: list[str] | None = _MISSING, ): """Initialize the workflow with a list of edges. @@ -198,9 +303,21 @@ class Workflow(DictConvertible): better observability and management. description: Optional description of what the workflow does. If the workflow is built using WorkflowBuilder, this will be the description of the builder. - output_executors: Optional list of executor IDs whose outputs will be considered workflow outputs. - If None or empty, all executor outputs are treated as workflow outputs. + output_from: List of executor IDs designated as workflow outputs, or + ``None`` for omitted-selection compatibility behavior when ``intermediate_output_from`` is also + ``None``. + intermediate_output_from: List of executor IDs designated as intermediate outputs. + In explicit designation mode, unlisted executor yields are hidden from + caller-facing output/intermediate events. + output_executors: Deprecated alias for ``output_from``. Will be removed + in a future version. + intermediate_executors: Deprecated alias for ``intermediate_output_from``. Will be + removed in a future version. """ + output_from = _coalesce_output_from_kwarg(output_from, output_executors) + intermediate_output_from = _coalesce_renamed_kwarg( + "intermediate_executors", intermediate_executors, "intermediate_output_from", intermediate_output_from + ) self.edge_groups = list(edge_groups) self.executors = dict(executors) self.start_executor_id = start_executor.id @@ -215,12 +332,20 @@ class Workflow(DictConvertible): self.graph_signature = self._compute_graph_signature() self.graph_signature_hash = self._hash_graph_signature(self.graph_signature) - # Output events (WorkflowEvent with type='output') from these executors are treated as workflow outputs. - # If None or empty, all executor outputs are considered workflow outputs. - self._output_executors = list(output_executors) if output_executors else list(self.executors.keys()) + # Single value type encodes omitted-selection compatibility vs explicit output-designation policy. + output_designation_ids = ( + frozenset(output_from) + if output_from is not None + else (frozenset[str]() if intermediate_output_from is not None else None) + ) + self._output_designation: OutputDesignation = OutputDesignation( + outputs=output_designation_ids, + intermediates=frozenset(intermediate_output_from or []), + ) # Store non-serializable runtime objects as private attributes self._runner_context = runner_context + self._runner_context.set_yield_output_classifier(self._output_designation.classify) self._state = State() self._runner: Runner = Runner( self.edge_groups, @@ -254,7 +379,12 @@ class Workflow(DictConvertible): "max_iterations": self.max_iterations, "edge_groups": [group.to_dict() for group in self.edge_groups], "executors": {executor_id: executor.to_dict() for executor_id, executor in self.executors.items()}, - "output_executors": self._output_executors, + "output_executors": ( + sorted(self._output_designation.outputs) if self._output_designation.outputs is not None else None + ), + "intermediate_executors": ( + sorted(self._output_designation.intermediates) if self._output_designation.outputs is not None else None + ), } if self.description is not None: @@ -289,8 +419,44 @@ class Workflow(DictConvertible): return self.executors[self.start_executor_id] def get_output_executors(self) -> list[Executor]: - """Get the list of output executors in the workflow.""" - return [self.executors[executor_id] for executor_id in self._output_executors] + """Get the list of output executors in the workflow. + + In omitted-selection compatibility mode (no explicit ``output_from``), returns every + executor in the workflow. In explicit mode, returns only the designated output executors. + """ + designated = self._output_designation.outputs + if designated is None: + return list(self.executors.values()) + return [self._get_designated_executor(executor_id, kind="Output") for executor_id in designated] + + def get_intermediate_executors(self) -> list[Executor]: + """Get the list of intermediate executors in the workflow.""" + return [ + self._get_designated_executor(executor_id, kind="Intermediate") + for executor_id in self._output_designation.intermediates + ] + + def _get_designated_executor(self, executor_id: str, *, kind: str) -> Executor: + try: + return self.executors[executor_id] + except KeyError as exc: + raise WorkflowValidationError( + f"{kind} executor '{executor_id}' is not present in the workflow graph", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) from exc + + def is_terminal_executor(self, executor_id: str) -> bool: + """Return True when ``executor_id``'s yields are labeled type='output'. + + Public read-only predicate over the workflow's output designation. External + observers (e.g., orchestration tests, DevUI mappers) should consult this rather + than re-encoding the rule as a set-membership check. + """ + return self._output_designation.is_terminal(executor_id) + + def is_intermediate_executor(self, executor_id: str) -> bool: + """Return True when ``executor_id``'s yields are labeled type='intermediate'.""" + return self._output_designation.is_intermediate(executor_id) def get_executors_list(self) -> list[Executor]: """Get the list of executors in the workflow.""" @@ -631,8 +797,6 @@ class Workflow(DictConvertible): function_invocation_kwargs=function_invocation_kwargs, client_kwargs=client_kwargs, ): - if event.type == "output" and not self._should_yield_output_event(event): - continue if event.type == "request_info" and event.request_id in (responses or {}): # Don't yield request_info events for which we have responses to send - # these are considered "handled". This prevents the caller from seeing @@ -825,22 +989,6 @@ class Workflow(DictConvertible): ) return {GLOBAL_KWARGS_KEY: dict(kwargs)} - def _should_yield_output_event(self, event: WorkflowEvent[Any]) -> bool: - """Determine if an output event should be yielded as a workflow output. - - Args: - event: The WorkflowEvent with type='output' to evaluate. - - Returns: - True if the event should be yielded as a workflow output, False otherwise. - """ - # If no specific output executors are defined, yield all outputs - if not self._output_executors: - return True - - # Check if the event's source executor is in the list of output executors - return event.executor_id in self._output_executors - # Graph signature helpers def _compute_graph_signature(self) -> dict[str, Any]: diff --git a/python/packages/core/agent_framework/_workflows/_workflow_builder.py b/python/packages/core/agent_framework/_workflows/_workflow_builder.py index 1a71b3a49b..942f3010f0 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_builder.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_builder.py @@ -3,8 +3,9 @@ import logging import sys import uuid +import warnings from collections.abc import Callable, Sequence -from typing import Any +from typing import Any, Literal from .._agents import SupportsAgentRun from ..observability import OtelAttr, capture_exception, create_workflow_span @@ -27,8 +28,12 @@ from ._edge import ( ) from ._executor import Executor from ._runner_context import InProcRunnerContext -from ._validation import validate_workflow_graph -from ._workflow import Workflow +from ._validation import ValidationTypeEnum, WorkflowValidationError, validate_workflow_graph +from ._workflow import ( + _MISSING, # pyright: ignore[reportPrivateUsage] + Workflow, + _coalesce_output_from_kwarg, # pyright: ignore[reportPrivateUsage] +) if sys.version_info >= (3, 11): from typing import Self # type: ignore # pragma: no cover @@ -38,6 +43,12 @@ else: logger = logging.getLogger(__name__) +_ALL_OUTPUTS: Literal["all"] = "all" +_ALL_OTHER_OUTPUTS: Literal["all_other"] = "all_other" +_OutputSelection = list[Executor | SupportsAgentRun] | Literal["all"] | None +_IntermediateOutputSelection = list[Executor | SupportsAgentRun] | Literal["all", "all_other"] | None +_AnyOutputSelection = _OutputSelection | _IntermediateOutputSelection + class WorkflowBuilder: """A builder class for constructing workflows. @@ -83,7 +94,9 @@ class WorkflowBuilder: *, start_executor: Executor | SupportsAgentRun, checkpoint_storage: CheckpointStorage | None = None, - output_executors: list[Executor | SupportsAgentRun] | None = None, + output_from: list[Executor | SupportsAgentRun] | Literal["all"] | None = _MISSING, + intermediate_output_from: _IntermediateOutputSelection = _MISSING, + output_executors: list[Executor | SupportsAgentRun] | None = _MISSING, ): """Initialize the WorkflowBuilder. @@ -98,9 +111,39 @@ class WorkflowBuilder: start_executor: The starting executor for the workflow. Can be an Executor instance or SupportsAgentRun instance. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. - output_executors: Optional list of executors whose outputs should be collected. - If not provided, outputs from all executors are collected. + output_from: Designates which executors emit workflow output + (``type='output'`` workflow events). Pass ``"all"`` to explicitly select every + executor with declared workflow output types. + intermediate_output_from: Designates which executors emit intermediate output + (``type='intermediate'`` workflow events). Pass ``"all"`` to select every executor + with declared workflow output types as intermediate (no executor emits ``output``). + Pass ``"all_other"`` to select every executor with declared workflow output types + that is not selected by ``output_from``. + If neither ``output_from`` nor ``intermediate_output_from`` is provided, + omitted-selection compatibility behavior applies and every ``yield_output`` produces + ``type='output'``. If either is provided, explicit mode applies: listed + workflow-output executors emit ``output``, listed intermediate executors emit + ``intermediate``, and unlisted executor yields are hidden. + + Output selection behavior: + - Omit both selections: every ``yield_output`` emits ``output`` for compatibility, + with a deprecation warning. + - ``output_from="all"``: every output-capable executor emits ``output``. + - ``output_from=[A]``: only A emits ``output``; other executor payloads are hidden. + - ``output_from=[A], intermediate_output_from="all_other"``: A emits ``output``; + all other output-capable executors emit ``intermediate``. + - ``intermediate_output_from="all_other"``: no executor emits ``output``; every + output-capable executor emits ``intermediate``. + - ``output_from=[], intermediate_output_from="all_other"``: no executor emits + ``output``; every output-capable executor emits ``intermediate``. + - ``output_from=[A], intermediate_output_from=[B, C]``: A emits ``output``; B and C + emit ``intermediate``; other executor payloads are hidden. + output_executors: **Deprecated** alias for ``output_from``. Will be removed in a + future version. """ + output_from = _coalesce_output_from_kwarg(output_from, output_executors) + if intermediate_output_from is _MISSING: + intermediate_output_from = None self._edge_groups: list[EdgeGroup] = [] self._executors: dict[str, Executor] = {} self._start_executor: Executor | None = None @@ -113,8 +156,13 @@ class WorkflowBuilder: # being created for the same agent. self._agent_wrappers: dict[str, Executor] = {} - # Output executors filter; if set, only outputs from these executors are yielded - self._output_executors: list[Executor | SupportsAgentRun] = output_executors if output_executors else [] + # ``None`` for both means omitted-selection compatibility behavior + # (every yield_output produces type='output'). + # If either is provided, explicit mode applies and unlisted executor yields are hidden. + self._output_from: _OutputSelection = self._coerce_output_from(output_from) + self._intermediate_output_from: _IntermediateOutputSelection = self._coerce_intermediate_output_from( + intermediate_output_from + ) # Set the start executor self._set_start_executor(start_executor) @@ -584,6 +632,96 @@ class WorkflowBuilder: if existing is not wrapped: self._add_executor(wrapped) + def _coerce_output_from(self, output_from: Any) -> _OutputSelection: + """Coerce workflow-output selection while preserving the explicit ``"all"`` literal.""" + if output_from is None: + return None + if output_from == _ALL_OUTPUTS: + return _ALL_OUTPUTS + if isinstance(output_from, str): + raise ValueError(f"Unsupported output_from literal {output_from!r}; use 'all' or a list of executors.") + return list(output_from) + + def _coerce_intermediate_output_from(self, intermediate_output_from: Any) -> _IntermediateOutputSelection: + """Coerce intermediate-output selection and reject output-only literals.""" + if intermediate_output_from is None: + return None + if isinstance(intermediate_output_from, str): + if intermediate_output_from == _ALL_OUTPUTS: + return _ALL_OUTPUTS + if intermediate_output_from == _ALL_OTHER_OUTPUTS: + return _ALL_OTHER_OUTPUTS + raise ValueError( + f"Unsupported intermediate_output_from literal {intermediate_output_from!r}; " + "use 'all', 'all_other', or a list of executors." + ) + return list(intermediate_output_from) + + def _resolve_designated_executor_ids( + self, + designated: _AnyOutputSelection, + ) -> list[str] | None: + """Resolve an optional designation list into executor IDs without mutating the graph.""" + if designated is None: + return None + if designated == _ALL_OUTPUTS: + return [executor_id for executor_id, executor in self._executors.items() if executor.workflow_output_types] + if designated == _ALL_OTHER_OUTPUTS: + raise ValueError("intermediate_output_from='all_other' must be expanded relative to output_from.") + ids: list[str] = [] + for item in designated: + if isinstance(item, Executor): + ids.append(item.id) + elif isinstance(item, SupportsAgentRun): + ids.append(resolve_agent_id(item)) + else: + raise TypeError( + "WorkflowBuilder expected designation entries to be Executor or SupportsAgentRun instances; " + f"got {type(item).__name__}." + ) + return ids + + def _validate_designation_lists( + self, + output_executor_ids: list[str] | None, + intermediate_executor_ids: list[str] | None, + ) -> None: + """Validate builder-level designation rules that need omitted-vs-explicit context.""" + explicit_mode = output_executor_ids is not None or intermediate_executor_ids is not None + if not explicit_mode: + return + + output_ids = output_executor_ids or [] + intermediate_ids = intermediate_executor_ids or [] + if not output_ids and not intermediate_ids: + raise WorkflowValidationError( + "Explicit workflow output designation must include at least one output or intermediate executor.", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) + + duplicate_outputs = sorted({executor_id for executor_id in output_ids if output_ids.count(executor_id) > 1}) + if duplicate_outputs: + raise WorkflowValidationError( + f"Duplicate output executor designation(s): {duplicate_outputs}", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) + + duplicate_intermediates = sorted({ + executor_id for executor_id in intermediate_ids if intermediate_ids.count(executor_id) > 1 + }) + if duplicate_intermediates: + raise WorkflowValidationError( + f"Duplicate intermediate executor designation(s): {duplicate_intermediates}", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) + + overlap = sorted(set(output_ids).intersection(intermediate_ids)) + if overlap: + raise WorkflowValidationError( + f"Executors cannot be both output and intermediate designated: {overlap}", + validation_type=ValidationTypeEnum.OUTPUT_VALIDATION, + ) + def build(self) -> Workflow: """Build and return the constructed workflow. @@ -625,6 +763,43 @@ class WorkflowBuilder: # Workflows can be reused multiple times events2 = await workflow.run("world") print(events2.get_outputs()) # ['WORLD'] + + # Select one executor as Workflow Output. + workflow = WorkflowBuilder(start_executor=executor, output_from=[executor]).build() + events = await workflow.run("hello") + print(events.get_outputs()) # ['HELLO'] + print(events.get_intermediate_outputs()) # [] + + # Make one executor Workflow Output and every other output-capable executor Intermediate Output. + workflow = ( + WorkflowBuilder( + start_executor=planner, + output_from=[answerer], + intermediate_output_from="all_other", + ) + .add_edge(planner, answerer) + .build() + ) + events = await workflow.run("hello") + print(events.get_outputs()) # outputs from answerer + print(events.get_intermediate_outputs()) # outputs from planner + + # Build a progress-only workflow: no Workflow Output, all output-capable executors are intermediate. + workflow = ( + WorkflowBuilder(start_executor=planner, intermediate_output_from="all_other") + .add_edge(planner, answerer) + .build() + ) + events = await workflow.run("hello") + print(events.get_outputs()) # [] + print(events.get_intermediate_outputs()) # outputs from planner and answerer + + # Explicitly preserve all-output behavior without relying on omitted-selection compatibility. + workflow = ( + WorkflowBuilder(start_executor=planner, output_from="all").add_edge(planner, answerer).build() + ) + events = await workflow.run("hello") + print(events.get_outputs()) # outputs from planner and answerer """ # Create workflow build span that includes validation and workflow creation with create_workflow_span(OtelAttr.WORKFLOW_BUILD_SPAN) as span: @@ -637,19 +812,47 @@ class WorkflowBuilder: "Starting executor must be set via the start_executor constructor parameter before building." ) + if self._output_from is None and self._intermediate_output_from is None: + warnings.warn( + "WorkflowBuilder built without explicit output_from or intermediate_output_from; " + "every yield_output produces type='output' for compatibility. Pass output_from='all', " + "output_from=[...], or intermediate_output_from=[...] to opt into explicit designation - " + "explicit designation will be required in a future version.", + DeprecationWarning, + stacklevel=2, + ) + start_executor = self._start_executor executors = self._executors edge_groups = self._edge_groups - output_executors = [ex.id for ex in self._output_executors if isinstance(ex, Executor)] + [ - resolve_agent_id(agent) for agent in self._output_executors if isinstance(agent, SupportsAgentRun) - ] + output_ids = self._resolve_designated_executor_ids(self._output_from) + intermediate_output_ids: list[str] | None + if self._intermediate_output_from == _ALL_OTHER_OUTPUTS: + output_ids_for_all_other = output_ids or [] + intermediate_output_ids = [ + executor_id + for executor_id, executor in self._executors.items() + if executor.workflow_output_types and executor_id not in output_ids_for_all_other + ] + else: + intermediate_output_ids = self._resolve_designated_executor_ids(self._intermediate_output_from) + self._validate_designation_lists(output_ids, intermediate_output_ids) + + explicit_mode = output_ids is not None or intermediate_output_ids is not None + output_for_workflow: list[str] | None = output_ids if explicit_mode else None + if explicit_mode and output_for_workflow is None: + output_for_workflow = [] + intermediate_output_for_workflow: list[str] | None = intermediate_output_ids if explicit_mode else None + if explicit_mode and intermediate_output_for_workflow is None: + intermediate_output_for_workflow = [] # Perform validation before creating the workflow validate_workflow_graph( edge_groups, executors, start_executor, - output_executors, + output_for_workflow or [], + intermediate_output_for_workflow or [], ) # Add validation completed event @@ -666,7 +869,8 @@ class WorkflowBuilder: self._name, description=self._description, max_iterations=self._max_iterations, - output_executors=output_executors, + output_from=output_for_workflow, + intermediate_output_from=intermediate_output_for_workflow, ) build_attributes: dict[str, Any] = { OtelAttr.WORKFLOW_BUILDER_NAME: self._name, diff --git a/python/packages/core/agent_framework/_workflows/_workflow_context.py b/python/packages/core/agent_framework/_workflows/_workflow_context.py index 51add07a5c..bfc8601e5d 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_context.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_context.py @@ -201,6 +201,7 @@ def validate_workflow_context_annotation( # Event types reserved for framework lifecycle (not allowed from user code) _FRAMEWORK_LIFECYCLE_EVENT_TYPES: frozenset[str] = frozenset({"started", "status", "failed"}) +_OUTPUT_SELECTION_EVENT_TYPES: frozenset[str] = frozenset({"output", "intermediate"}) class WorkflowContext(Generic[OutT, W_OutT]): @@ -337,7 +338,20 @@ class WorkflowContext(Generic[OutT, W_OutT]): await self._runner_context.send_message(msg) async def yield_output(self, output: W_OutT) -> None: - """Set the output of the workflow. + """Yield an output from this executor. + + The framework labels the resulting workflow event based on the workflow's explicit + output designation: + + - Omitted-selection compatibility behavior: every yield produces ``type='output'``. + - Explicit mode: output-designated executors produce ``type='output'``, + intermediate-designated executors produce ``type='intermediate'``, and + unlisted executor yields are hidden from caller-facing events. + + Whether a given executor produces ``output`` or ``intermediate`` events is fixed at + workflow-build time via ``output_from`` / ``intermediate_output_from`` on + :class:`WorkflowBuilder`; an executor cannot vary the label per yield. To change an + executor's role, list it under a different designation when building the workflow. Args: output: The output to yield. This must conform to the workflow output type(s) @@ -347,12 +361,24 @@ class WorkflowContext(Generic[OutT, W_OutT]): # (deepcopy to capture state at yield time) self._yielded_outputs.append(copy.deepcopy(output)) + event_type = self._runner_context.classify_yielded_output(self._executor_id) + if event_type is None: + return + with _framework_event_origin(): - event = WorkflowEvent.output(self._executor_id, output) + event = WorkflowEvent(event_type, executor_id=self._executor_id, data=output) await self._runner_context.add_event(event) async def add_event(self, event: WorkflowEvent[Any]) -> None: """Add an event to the workflow context.""" + if event.origin == WorkflowEventSource.EXECUTOR and event.type in _OUTPUT_SELECTION_EVENT_TYPES: + warning_msg = ( + f"Executor '{self._executor_id}' attempted to emit a '{event.type}' event directly, " + "which is reserved for ctx.yield_output(). The event was ignored." + ) + logger.warning(warning_msg) + await self._runner_context.add_event(WorkflowEvent.warning(warning_msg)) + return if event.origin == WorkflowEventSource.EXECUTOR and event.type in _FRAMEWORK_LIFECYCLE_EVENT_TYPES: warning_msg = ( f"Executor '{self._executor_id}' attempted to emit a '{event.type}' event, " diff --git a/python/packages/core/agent_framework/_workflows/_workflow_executor.py b/python/packages/core/agent_framework/_workflows/_workflow_executor.py index 847b44863c..e131533429 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_executor.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_executor.py @@ -16,6 +16,7 @@ from ._const import GLOBAL_KWARGS_KEY, WORKFLOW_RUN_KWARGS_KEY from ._events import ( WorkflowEvent, WorkflowRunState, + _framework_event_origin, # type: ignore[reportPrivateUsage] ) from ._executor import Executor, handler from ._request_info_mixin import response_handler @@ -552,10 +553,12 @@ class WorkflowExecutor(Executor): # Collect all events from the workflow request_info_events = result.get_request_info_events() outputs = result.get_outputs() + intermediate_outputs = result.get_intermediate_outputs() workflow_run_state = result.get_final_state() logger.debug( f"WorkflowExecutor {self.id} processing workflow result with " - f"{len(outputs)} outputs and {len(request_info_events)} request info events. " + f"{len(outputs)} outputs, {len(intermediate_outputs)} intermediate outputs, " + f"and {len(request_info_events)} request info events. " f"Workflow run state: {workflow_run_state}" ) @@ -566,6 +569,19 @@ class WorkflowExecutor(Executor): else: await asyncio.gather(*[ctx.send_message(output) for output in outputs]) + # Pipe sub-workflow intermediate emissions up through the parent's event stream. + # Bypasses the parent's yield-output classifier so the 'intermediate' label is preserved + # across the encapsulation boundary; uses this WorkflowExecutor's id as the source + # so outer callers don't need to know the sub-workflow's internal executor layout. + if intermediate_outputs: + + async def _forward_intermediate_output(output: Any) -> None: + with _framework_event_origin(): + event = WorkflowEvent("intermediate", executor_id=self.id, data=output) + await ctx.add_event(event) + + await asyncio.gather(*[_forward_intermediate_output(output) for output in intermediate_outputs]) + # Process request info events for event in request_info_events: request_id = event.request_id diff --git a/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py b/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py index 07a37f9617..9f17af9e4e 100644 --- a/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py +++ b/python/packages/core/tests/workflow/test_agent_executor_tool_calls.py @@ -272,9 +272,7 @@ async def test_agent_executor_tool_call_with_approval() -> None: tools=[mock_tool_requiring_approval], ) - workflow = ( - WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build() - ) + workflow = WorkflowBuilder(start_executor=agent, output_from=[test_executor]).add_edge(agent, test_executor).build() # Act events = await workflow.run("Invoke tool requiring approval") @@ -343,9 +341,7 @@ async def test_agent_executor_parallel_tool_call_with_approval() -> None: tools=[mock_tool_requiring_approval], ) - workflow = ( - WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build() - ) + workflow = WorkflowBuilder(start_executor=agent, output_from=[test_executor]).add_edge(agent, test_executor).build() # Act events = await workflow.run("Invoke tool requiring approval") @@ -512,9 +508,7 @@ async def test_agent_executor_declaration_only_tool_emits_request_info() -> None tools=[declaration_only_tool], ) - workflow = ( - WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build() - ) + workflow = WorkflowBuilder(start_executor=agent, output_from=[test_executor]).add_edge(agent, test_executor).build() # Act events = await workflow.run("Use the client side tool") @@ -587,9 +581,7 @@ async def test_agent_executor_parallel_declaration_only_tool_emits_request_info( tools=[declaration_only_tool], ) - workflow = ( - WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build() - ) + workflow = WorkflowBuilder(start_executor=agent, output_from=[test_executor]).add_edge(agent, test_executor).build() # Act events = await workflow.run("Use the client side tool") diff --git a/python/packages/core/tests/workflow/test_agent_run_event_typing.py b/python/packages/core/tests/workflow/test_agent_run_event_typing.py index 2b16f01258..b40e5d91ba 100644 --- a/python/packages/core/tests/workflow/test_agent_run_event_typing.py +++ b/python/packages/core/tests/workflow/test_agent_run_event_typing.py @@ -9,7 +9,7 @@ from agent_framework._workflows._events import WorkflowEvent def test_workflow_event_with_agent_response_data_type() -> None: """Verify WorkflowEvent[AgentResponse].data is typed as AgentResponse.""" response = AgentResponse(messages=[Message(role="assistant", contents=["Hello"])]) - event: WorkflowEvent[AgentResponse] = WorkflowEvent.emit(executor_id="test", data=response) + event: WorkflowEvent[AgentResponse] = WorkflowEvent("intermediate", executor_id="test", data=response) # This assignment should pass type checking without a cast data: AgentResponse = event.data @@ -20,7 +20,7 @@ def test_workflow_event_with_agent_response_data_type() -> None: def test_workflow_event_with_agent_response_update_data_type() -> None: """Verify WorkflowEvent[AgentResponseUpdate].data is typed as AgentResponseUpdate.""" update = AgentResponseUpdate() - event: WorkflowEvent[AgentResponseUpdate] = WorkflowEvent.emit(executor_id="test", data=update) + event: WorkflowEvent[AgentResponseUpdate] = WorkflowEvent("intermediate", executor_id="test", data=update) # This assignment should pass type checking without a cast data: AgentResponseUpdate = event.data @@ -30,7 +30,7 @@ def test_workflow_event_with_agent_response_update_data_type() -> None: def test_workflow_event_repr() -> None: """Verify WorkflowEvent.__repr__ uses consistent format.""" response = AgentResponse(messages=[Message(role="assistant", contents=["Hello"])]) - event: WorkflowEvent[AgentResponse] = WorkflowEvent.emit(executor_id="test", data=response) + event: WorkflowEvent[AgentResponse] = WorkflowEvent("intermediate", executor_id="test", data=response) repr_str = repr(event) assert "WorkflowEvent" in repr_str diff --git a/python/packages/core/tests/workflow/test_full_conversation.py b/python/packages/core/tests/workflow/test_full_conversation.py index 79d8626bc2..27b3dc4019 100644 --- a/python/packages/core/tests/workflow/test_full_conversation.py +++ b/python/packages/core/tests/workflow/test_full_conversation.py @@ -177,7 +177,7 @@ async def test_agent_executor_populates_full_conversation_non_streaming() -> Non agent_exec = AgentExecutor(agent, id="agent1-exec") capturer = _CaptureFullConversation(id="capture") - wf = WorkflowBuilder(start_executor=agent_exec, output_executors=[capturer]).add_edge(agent_exec, capturer).build() + wf = WorkflowBuilder(start_executor=agent_exec, output_from=[capturer]).add_edge(agent_exec, capturer).build() # Act: use run() to test non-streaming mode result = await wf.run("hello world") @@ -344,7 +344,7 @@ async def test_agent_executor_full_conversation_round_trip_does_not_duplicate_hi coordinator = _RoundTripCoordinator(target_agent_id="writer_agent") wf = ( - WorkflowBuilder(start_executor=agent_exec, output_executors=[coordinator]) + WorkflowBuilder(start_executor=agent_exec, output_from=[coordinator]) .add_edge(agent_exec, coordinator) .add_edge(coordinator, agent_exec) .build() @@ -450,7 +450,7 @@ async def test_run_request_with_full_history_clears_service_session_id() -> None coordinator = _FullHistoryReplayCoordinator(id="coord", target_exec=spy_exec) wf = ( - WorkflowBuilder(start_executor=tool_exec, output_executors=[coordinator]) + WorkflowBuilder(start_executor=tool_exec, output_from=[coordinator]) .add_edge(tool_exec, coordinator) .add_edge(coordinator, spy_exec) .build() @@ -478,7 +478,7 @@ async def test_from_response_preserves_service_session_id() -> None: # Simulate a prior run on the spy executor. spy_exec._session.service_session_id = "resp_PREVIOUS_RUN" # pyright: ignore[reportPrivateUsage] - wf = WorkflowBuilder(start_executor=tool_exec, output_executors=[spy_exec]).add_edge(tool_exec, spy_exec).build() + wf = WorkflowBuilder(start_executor=tool_exec, output_from=[spy_exec]).add_edge(tool_exec, spy_exec).build() result = await wf.run("start") assert result.get_outputs() is not None @@ -517,7 +517,7 @@ async def test_with_text_preserves_full_conversation_through_custom_executor() - capturer = _CaptureFullConversation(id="capture") wf = ( - WorkflowBuilder(start_executor=agent1, output_executors=[capturer]) + WorkflowBuilder(start_executor=agent1, output_from=[capturer]) .add_chain([agent1, agent2, _upper_case_executor, agent3, capturer]) .build() ) diff --git a/python/packages/core/tests/workflow/test_functional_workflow.py b/python/packages/core/tests/workflow/test_functional_workflow.py index ba465ffe0b..6502a0e353 100644 --- a/python/packages/core/tests/workflow/test_functional_workflow.py +++ b/python/packages/core/tests/workflow/test_functional_workflow.py @@ -165,13 +165,13 @@ class TestEventEmission: @workflow async def pipeline(x: int, ctx: RunContext) -> int: - await ctx.add_event(WorkflowEvent.emit("pipeline", "custom_data")) + await ctx.add_event(WorkflowEvent("intermediate", executor_id="pipeline", data="custom_data")) return x result = await pipeline.run(1) - data_events = [e for e in result if e.type == "data"] - assert len(data_events) == 1 - assert data_events[0].data == "custom_data" + intermediate_events = [e for e in result if e.type == "intermediate"] + assert len(intermediate_events) == 1 + assert intermediate_events[0].data == "custom_data" # --------------------------------------------------------------------------- diff --git a/python/packages/core/tests/workflow/test_output_designation.py b/python/packages/core/tests/workflow/test_output_designation.py new file mode 100644 index 0000000000..cd4b23ee21 --- /dev/null +++ b/python/packages/core/tests/workflow/test_output_designation.py @@ -0,0 +1,137 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for the ``OutputDesignation`` value type and the ``Workflow.is_terminal_executor`` +public predicate that delegates to it. + +The states the value type encodes: +- Omitted-selection compatibility: ``outputs=None`` -> every executor is terminal. +- Explicit: disjoint ``outputs`` and ``intermediates`` sets classify listed executors, + and hide unlisted executors. +""" + +from __future__ import annotations + +import pytest +from typing_extensions import Never + +from agent_framework import ( + Message, + WorkflowBuilder, + WorkflowContext, + WorkflowValidationError, + executor, +) +from agent_framework._workflows._runner_context import InProcRunnerContext +from agent_framework._workflows._workflow import OutputDesignation, Workflow + +# --------------------------------------------------------------------------- +# OutputDesignation value type +# --------------------------------------------------------------------------- + + +def test_omitted_selection_designation_marks_every_executor_as_terminal() -> None: + designation = OutputDesignation() # designated defaults to None + assert designation.outputs is None + assert designation.is_terminal("anything") + assert designation.is_terminal("else") + assert designation.classify("anything") == "output" + + +def test_strict_empty_designation_marks_no_executor_as_terminal() -> None: + designation = OutputDesignation(outputs=frozenset()) + assert designation.outputs == frozenset() + assert not designation.is_terminal("anything") + assert not designation.is_terminal("else") + assert designation.classify("anything") is None + + +def test_strict_designated_set_only_terminal_for_members() -> None: + designation = OutputDesignation(outputs=frozenset({"alpha", "beta"}), intermediates=frozenset({"gamma"})) + assert designation.is_terminal("alpha") + assert designation.is_terminal("beta") + assert not designation.is_terminal("gamma") + assert designation.is_intermediate("gamma") + assert designation.classify("alpha") == "output" + assert designation.classify("gamma") == "intermediate" + assert designation.classify("delta") is None + + +def test_designation_is_frozen() -> None: + from dataclasses import FrozenInstanceError + + designation = OutputDesignation(outputs=frozenset({"alpha"})) + with pytest.raises(FrozenInstanceError): + designation.outputs = frozenset({"beta"}) # type: ignore[misc] + + +# --------------------------------------------------------------------------- +# Workflow.is_terminal_executor delegates to the designation +# --------------------------------------------------------------------------- + + +@executor +async def _emit_one(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("hello") + + +@executor +async def _downstream(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("downstream") + + +def test_is_terminal_executor_omitted_selection_returns_true_for_any_id() -> None: + """Omitted-selection compatibility behavior: every executor is terminal.""" + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + workflow = WorkflowBuilder(start_executor=_emit_one).build() + assert workflow.is_terminal_executor(_emit_one.id) + assert workflow.is_terminal_executor("anything-else") + + +def test_is_intermediate_executor_explicit_list_returns_true_only_for_designated() -> None: + """Explicit mode tracks intermediate-designated executors separately.""" + workflow = WorkflowBuilder(start_executor=_emit_one, intermediate_output_from=[_emit_one]).build() + assert not workflow.is_terminal_executor(_emit_one.id) + assert not workflow.is_terminal_executor("nope") + assert workflow.is_intermediate_executor(_emit_one.id) + assert not workflow.is_intermediate_executor("nope") + + +def test_is_terminal_executor_strict_list_returns_true_only_for_designated() -> None: + """Strict mode with a designated list: only listed executors are terminal.""" + workflow = ( + WorkflowBuilder(start_executor=_emit_one, output_from=[_emit_one]).add_edge(_emit_one, _downstream).build() + ) + assert workflow.is_terminal_executor(_emit_one.id) + assert not workflow.is_terminal_executor(_downstream.id) + + +def test_get_output_executors_throws_when_designation_references_missing_executor() -> None: + workflow = Workflow( + [], + {_emit_one.id: _emit_one}, + _emit_one, + InProcRunnerContext(), + "test", + output_from=["missing"], + ) + + with pytest.raises(WorkflowValidationError, match="Output executor 'missing' is not present"): + workflow.get_output_executors() + + +def test_get_intermediate_executors_throws_when_designation_references_missing_executor() -> None: + workflow = Workflow( + [], + {_emit_one.id: _emit_one}, + _emit_one, + InProcRunnerContext(), + "test", + output_from=[], + intermediate_output_from=["missing"], + ) + + with pytest.raises(WorkflowValidationError, match="Intermediate executor 'missing' is not present"): + workflow.get_intermediate_executors() diff --git a/python/packages/core/tests/workflow/test_output_executors_contract.py b/python/packages/core/tests/workflow/test_output_executors_contract.py new file mode 100644 index 0000000000..31f4f946b7 --- /dev/null +++ b/python/packages/core/tests/workflow/test_output_executors_contract.py @@ -0,0 +1,287 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for the explicit output/intermediate selection contract on WorkflowBuilder.""" + +from __future__ import annotations + +import warnings +from typing import Any + +import pytest +from typing_extensions import Never + +from agent_framework import ( + Message, + WorkflowBuilder, + WorkflowContext, + WorkflowValidationError, + executor, +) + + +@executor +async def _emit_one(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("hello") + + +@executor +async def _start(messages: list[Message], ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output("from-start") + await ctx.send_message("downstream") + + +@executor +async def _downstream(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("from-downstream") + + +def test_designation_unset_emits_deprecation_warning() -> None: + """State A: WorkflowBuilder built without explicit designation warns.""" + with pytest.warns(DeprecationWarning, match="output_from or intermediate_output_from") as warning_info: + WorkflowBuilder(start_executor=_emit_one).build() + assert str(warning_info[0].message) == ( + "WorkflowBuilder built without explicit output_from or intermediate_output_from; " + "every yield_output produces type='output' for compatibility. Pass output_from='all', " + "output_from=[...], or intermediate_output_from=[...] to opt into explicit designation - " + "explicit designation will be required in a future version." + ) + + +@pytest.mark.asyncio +async def test_designation_unset_preserves_compatibility_all_output_behavior() -> None: + """Omitted designation keeps compatibility all-output behavior while warning.""" + with pytest.warns(DeprecationWarning, match="output_from or intermediate_output_from"): + workflow = WorkflowBuilder(start_executor=_start).add_edge(_start, _downstream).build() + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == ["from-start", "from-downstream"] + assert result.get_intermediate_outputs() == [] + + +@pytest.mark.asyncio +async def test_output_from_all_emits_all_outputs_without_omitted_selection_warning() -> None: + """Explicit all-output designation emits every executor payload without omitted-selection warning.""" + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + workflow = WorkflowBuilder(start_executor=_start, output_from="all").add_edge(_start, _downstream).build() + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == ["from-start", "from-downstream"] + assert result.get_intermediate_outputs() == [] + + +@pytest.mark.asyncio +async def test_output_from_all_with_empty_intermediate_list_is_valid() -> None: + """Explicit all-output plus an empty intermediate list is a concrete no-intermediate selection.""" + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + workflow = ( + WorkflowBuilder(start_executor=_start, output_from="all", intermediate_output_from=[]) + .add_edge(_start, _downstream) + .build() + ) + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == ["from-start", "from-downstream"] + assert result.get_intermediate_outputs() == [] + + +@pytest.mark.asyncio +async def test_intermediate_output_from_all_other_marks_non_outputs_as_intermediate() -> None: + """All-other intermediate designation classifies every non-output executor yield as intermediate.""" + workflow = ( + WorkflowBuilder( + start_executor=_start, + output_from=[_downstream], + intermediate_output_from="all_other", + ) + .add_edge(_start, _downstream) + .build() + ) + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == ["from-downstream"] + assert result.get_intermediate_outputs() == ["from-start"] + + +@pytest.mark.asyncio +async def test_all_other_streaming_events_mark_non_outputs_as_intermediate() -> None: + """All-other emits intermediate events while streaming, not just in collected results.""" + workflow = ( + WorkflowBuilder( + start_executor=_start, + output_from=[_downstream], + intermediate_output_from="all_other", + ) + .add_edge(_start, _downstream) + .build() + ) + outputs: list[str] = [] + intermediates: list[str] = [] + + async for event in workflow.run([Message(role="user", contents=["hi"])], stream=True): + if event.type == "output": + outputs.append(event.data) + elif event.type == "intermediate": + intermediates.append(event.data) + + assert outputs == ["from-downstream"] + assert intermediates == ["from-start"] + + +def test_all_other_expands_to_concrete_intermediate_executor_selection_at_build_time() -> None: + """The runner receives concrete executor IDs after all-other expansion.""" + workflow = ( + WorkflowBuilder( + start_executor=_start, + output_from=[_downstream], + intermediate_output_from="all_other", + ) + .add_edge(_start, _downstream) + .build() + ) + + assert {executor.id for executor in workflow.get_output_executors()} == {_downstream.id} + assert {executor.id for executor in workflow.get_intermediate_executors()} == {_start.id} + assert workflow.is_intermediate_executor(_start.id) + assert not workflow.is_intermediate_executor(_downstream.id) + + +@pytest.mark.asyncio +async def test_all_other_with_omitted_output_from_emits_only_intermediate_outputs() -> None: + """All-other intermediate designation opts out of omitted-selection all-output behavior.""" + workflow = ( + WorkflowBuilder( + start_executor=_start, + intermediate_output_from="all_other", + ) + .add_edge(_start, _downstream) + .build() + ) + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == [] + assert result.get_intermediate_outputs() == ["from-start", "from-downstream"] + + +@pytest.mark.asyncio +async def test_all_other_with_empty_output_from_emits_only_intermediate_outputs() -> None: + """All-other intermediate designation treats an empty output list as selecting no workflow outputs.""" + workflow = ( + WorkflowBuilder( + start_executor=_start, + output_from=[], + intermediate_output_from="all_other", + ) + .add_edge(_start, _downstream) + .build() + ) + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == [] + assert result.get_intermediate_outputs() == ["from-start", "from-downstream"] + + +@pytest.mark.asyncio +async def test_all_other_with_output_from_all_expands_to_empty_intermediate_selection() -> None: + """All-other is empty when every output-capable executor is already selected as workflow output.""" + workflow = ( + WorkflowBuilder( + start_executor=_start, + output_from="all", + intermediate_output_from="all_other", + ) + .add_edge(_start, _downstream) + .build() + ) + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == ["from-start", "from-downstream"] + assert result.get_intermediate_outputs() == [] + + +@pytest.mark.asyncio +async def test_intermediate_output_from_all_routes_every_yield_to_intermediate() -> None: + """``intermediate_output_from="all"`` designates every output-capable executor as intermediate.""" + workflow = ( + WorkflowBuilder(start_executor=_start, intermediate_output_from="all").add_edge(_start, _downstream).build() + ) + + result = await workflow.run([Message(role="user", contents=["hi"])]) + + assert result.get_outputs() == [] + assert result.get_intermediate_outputs() == ["from-start", "from-downstream"] + + +def test_output_from_all_other_is_rejected() -> None: + """The all-other literal is only valid for intermediate output selection.""" + with pytest.raises(ValueError, match="output_from.*all_other"): + WorkflowBuilder(start_executor=_emit_one, output_from="all_other") # type: ignore[arg-type] + + +@pytest.mark.parametrize( + ("output_from", "intermediate_output_from"), + [([_emit_one], None), (None, [_emit_one]), ([], [_emit_one])], + ids=["output_list", "intermediate_list", "empty_output_with_intermediate"], +) +def test_explicit_designation_with_executor_does_not_warn(output_from, intermediate_output_from) -> None: + """State B: any explicit designation with at least one executor opts into explicit mode without warning.""" + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + WorkflowBuilder( + start_executor=_emit_one, + output_from=output_from, + intermediate_output_from=intermediate_output_from, + ).build() + + +@pytest.mark.parametrize( + ("output_from", "intermediate_output_from"), + [([], None), (None, []), ([], [])], + ids=["empty_output", "empty_intermediate", "both_empty"], +) +def test_empty_explicit_designation_fails(output_from, intermediate_output_from) -> None: + """State C: explicit mode needs at least one output or intermediate executor.""" + with pytest.raises(WorkflowValidationError, match="at least one output or intermediate executor"): + WorkflowBuilder( + start_executor=_emit_one, + output_from=output_from, + intermediate_output_from=intermediate_output_from, + ).build() + + +def test_passing_both_output_executors_and_output_from_raises_type_error() -> None: + """State D: supplying a deprecated alias and the canonical kwarg is unambiguous user error.""" + with pytest.raises(TypeError, match="Cannot pass multiple workflow output selection parameters"): + WorkflowBuilder( + start_executor=_emit_one, + output_executors=[_emit_one], + output_from=[_emit_one], + ) + + +def test_intermediate_executors_builder_parameter_is_not_public() -> None: + """The branch-only intermediate_executors builder parameter is not supported.""" + builder_type: Any = WorkflowBuilder + with pytest.raises(TypeError, match="unexpected keyword argument 'intermediate_executors'"): + builder_type( + start_executor=_emit_one, + intermediate_executors=[_emit_one], + ) + + +def test_final_output_from_builder_parameter_is_not_public() -> None: + """The branch-only final_output_from builder parameter is not supported.""" + builder_type: Any = WorkflowBuilder + with pytest.raises(TypeError, match="unexpected keyword argument 'final_output_from'"): + builder_type( + start_executor=_emit_one, + final_output_from=[_emit_one], + ) diff --git a/python/packages/core/tests/workflow/test_runner.py b/python/packages/core/tests/workflow/test_runner.py index a42e94f39d..4fef26bd2d 100644 --- a/python/packages/core/tests/workflow/test_runner.py +++ b/python/packages/core/tests/workflow/test_runner.py @@ -158,7 +158,9 @@ async def test_runner_run_iteration_preserves_message_order_per_edge_runner() -> def __init__(self) -> None: self.received: list[int] = [] - async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: + async def send_message( + self, message: WorkflowMessage, state: State, ctx: RunnerContext, *args: object, **kwargs: object + ) -> bool: message_data = message.data assert isinstance(message_data, MockMessage) self.received.append(message_data.data) @@ -188,7 +190,9 @@ async def test_runner_run_iteration_delivers_different_edge_runners_concurrently self.release = asyncio.Event() self.call_count = 0 - async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: + async def send_message( + self, message: WorkflowMessage, state: State, ctx: RunnerContext, *args: object, **kwargs: object + ) -> bool: self.call_count += 1 self.started.set() await self.release.wait() @@ -199,7 +203,9 @@ async def test_runner_run_iteration_delivers_different_edge_runners_concurrently self.probe_completed = asyncio.Event() self.call_count = 0 - async def send_message(self, message: WorkflowMessage, state: State, ctx: RunnerContext) -> bool: + async def send_message( + self, message: WorkflowMessage, state: State, ctx: RunnerContext, *args: object, **kwargs: object + ) -> bool: self.call_count += 1 self.probe_completed.set() return True @@ -766,7 +772,7 @@ async def test_runner_with_pre_loop_events(): runner = Runner([], {}, state, ctx, "test_name", graph_signature_hash="test_hash") # Add an event before running - await ctx.add_event(WorkflowEvent.output(executor_id="test_executor", data="pre-loop-output")) + await ctx.add_event(WorkflowEvent("output", executor_id="test_executor", data="pre-loop-output")) events: list[WorkflowEvent] = [] async for event in runner.run_until_convergence(): @@ -891,7 +897,7 @@ class ExecutorThatFailsWithEvents(Executor): # First emit an output event to the workflow context await ctx.yield_output(f"output-before-failure-{message.data}") # Add some events directly to the runner context - await self._runner_ctx.add_event(WorkflowEvent.output(executor_id=self.id, data="pending-event")) + await self._runner_ctx.add_event(WorkflowEvent("output", executor_id=self.id, data="pending-event")) # Fail on the specified iteration if self._iteration_count >= self._fail_on_iteration: raise RuntimeError("Executor failed with pending events") diff --git a/python/packages/core/tests/workflow/test_serialization.py b/python/packages/core/tests/workflow/test_serialization.py index 55284db407..ed6316ecbe 100644 --- a/python/packages/core/tests/workflow/test_serialization.py +++ b/python/packages/core/tests/workflow/test_serialization.py @@ -799,3 +799,48 @@ def test_comprehensive_edge_groups_workflow_serialization() -> None: assert len(fan_in_groups[0]["edges"]) == 2, "FanInEdgeGroup should have 2 edges (from parallel_1 and parallel_2)" for single_group in single_groups: assert len(single_group["edges"]) == 1, "Each SingleEdgeGroup should have exactly 1 edge" + + +def test_to_dict_preserves_compatibility_wire_keys_for_output_designation() -> None: + """to_dict() must emit the compatibility wire keys regardless of the Python kwarg names. + + The Python API renamed ``output_executors`` -> ``output_from`` and + uses ``intermediate_output_from`` for intermediate selection, but the serialized + dict must keep the old keys so existing checkpoints stay readable. This is a + regression guard against accidental renames of the wire format. + """ + + class _Yielder(Executor): + @handler + async def handle(self, message: str, ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output(message) + await ctx.send_message(message) + + class _Terminal(Executor): + @handler + async def handle(self, message: str, ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output(f"final: {message}") + + start = _Yielder(id="start") + progress = _Yielder(id="progress") + final = _Terminal(id="final") + + workflow = ( + WorkflowBuilder( + start_executor=start, + output_from=[final], + intermediate_output_from=[progress], + ) + .add_edge(start, progress) + .add_edge(progress, final) + .build() + ) + + d = workflow.to_dict() + + assert "output_executors" in d, "wire key 'output_executors' must be preserved" + assert "intermediate_executors" in d, "wire key 'intermediate_executors' must be preserved" + assert "output_from" not in d, "new Python kwarg name must NOT leak into the wire format" + assert "intermediate_output_from" not in d, "new Python kwarg name must NOT leak into the wire format" + assert d["output_executors"] == ["final"] + assert d["intermediate_executors"] == ["progress"] diff --git a/python/packages/core/tests/workflow/test_strict_mode_event_labeling.py b/python/packages/core/tests/workflow/test_strict_mode_event_labeling.py new file mode 100644 index 0000000000..d1de5c3cb0 --- /dev/null +++ b/python/packages/core/tests/workflow/test_strict_mode_event_labeling.py @@ -0,0 +1,118 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for the runner's explicit output selection event labeling.""" + +from __future__ import annotations + +import warnings +from typing import Any + +import pytest +from typing_extensions import Never + +from agent_framework import ( + Message, + WorkflowBuilder, + WorkflowContext, + executor, +) + + +@executor +async def _start(messages: list[Message], ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output("from-start") + await ctx.send_message("downstream") + + +@executor +async def _downstream(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("from-downstream") + + +def _input_msg() -> list[Message]: + return [Message(role="user", contents=["hi"])] + + +@pytest.mark.asyncio +async def test_strict_mode_designated_executor_emits_output_events() -> None: + """Output-designated executor yields produce type='output' events.""" + workflow = WorkflowBuilder(start_executor=_start, output_from=[_start]).add_edge(_start, _downstream).build() + output_events: list[Any] = [] + intermediate_events: list[Any] = [] + async for event in workflow.run(_input_msg(), stream=True): + if event.type == "output": + output_events.append(event) + elif event.type == "intermediate": + intermediate_events.append(event) + + assert any(ev.data == "from-start" for ev in output_events), "designated executor's yield is type='output'" + assert intermediate_events == [] + assert all(ev.data != "from-downstream" for ev in output_events), "unlisted executor yield is hidden" + + +@pytest.mark.asyncio +async def test_intermediate_designated_executor_emits_intermediate_events() -> None: + """Intermediate-designated executor yields produce type='intermediate' events.""" + workflow = ( + WorkflowBuilder(start_executor=_start, intermediate_output_from=[_downstream]) + .add_edge(_start, _downstream) + .build() + ) + output_events: list[Any] = [] + intermediate_events: list[Any] = [] + async for event in workflow.run(_input_msg(), stream=True): + if event.type == "output": + output_events.append(event) + elif event.type == "intermediate": + intermediate_events.append(event) + + assert len(output_events) == 0 + assert {ev.data for ev in intermediate_events} == {"from-downstream"} + + +@pytest.mark.asyncio +async def test_omitted_selection_keeps_all_yields_as_output() -> None: + """Omitted output selection preserves today's behavior: all yields are type='output'.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + workflow = WorkflowBuilder(start_executor=_start).add_edge(_start, _downstream).build() + output_events: list[Any] = [] + intermediate_events: list[Any] = [] + async for event in workflow.run(_input_msg(), stream=True): + if event.type == "output": + output_events.append(event) + elif event.type == "intermediate": + intermediate_events.append(event) + + assert {ev.data for ev in output_events} == {"from-start", "from-downstream"} + assert len(intermediate_events) == 0 + + +@pytest.mark.asyncio +async def test_strict_mode_get_outputs_returns_only_designated() -> None: + """WorkflowRunResult.get_outputs() returns only output-designated payloads.""" + workflow = ( + WorkflowBuilder( + start_executor=_start, + output_from=[_downstream], + intermediate_output_from=[_start], + ) + .add_edge(_start, _downstream) + .build() + ) + result = await workflow.run(_input_msg()) + assert result.get_outputs() == ["from-downstream"] + assert result.get_intermediate_outputs() == ["from-start"] + + +@pytest.mark.asyncio +async def test_hidden_yields_remain_in_executor_completion_events() -> None: + """Hidden yield_output payloads stay available through executor_completed observability.""" + workflow = WorkflowBuilder(start_executor=_start, output_from=[_downstream]).add_edge(_start, _downstream).build() + result = await workflow.run(_input_msg()) + assert result.get_outputs() == ["from-downstream"] + assert result.get_intermediate_outputs() == [] + assert not any(event.type in {"output", "intermediate"} and event.data == "from-start" for event in result) + completed = [event for event in result if event.type == "executor_completed" and event.executor_id == _start.id] + assert completed + assert completed[0].data == ["downstream", "from-start"] diff --git a/python/packages/core/tests/workflow/test_sub_workflow.py b/python/packages/core/tests/workflow/test_sub_workflow.py index 666e82f4d7..7bf38a06f3 100644 --- a/python/packages/core/tests/workflow/test_sub_workflow.py +++ b/python/packages/core/tests/workflow/test_sub_workflow.py @@ -617,3 +617,75 @@ async def test_sub_workflow_checkpoint_restore_no_duplicate_requests() -> None: # Key assertion: Only the second request should be received, not a duplicate of the first assert len(request_events) == 1 assert request_events[0].data.prompt == "Second request" + + +async def test_sub_workflow_intermediate_outputs_propagate_to_parent() -> None: + """A child workflow's intermediate emissions must bubble up through the parent. + + Regression guard for the bug where WorkflowExecutor._process_workflow_result only + forwarded result.get_outputs() and silently dropped result.get_intermediate_outputs(). + The forwarded event must carry the WorkflowExecutor's own id as the source so outer + callers don't have to know the child's internal executor layout, and it must keep + type='intermediate' regardless of how the parent designates the WorkflowExecutor. + """ + + class _ProgressEmitter(Executor): + def __init__(self) -> None: + super().__init__(id="progress_emitter") + + @handler + async def run(self, message: str, ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output(f"progress: {message}") + await ctx.send_message(message) + + class _Finalizer(Executor): + def __init__(self) -> None: + super().__init__(id="finalizer") + + @handler + async def run(self, message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(f"final: {message}") + + progress = _ProgressEmitter() + finalizer = _Finalizer() + child = ( + WorkflowBuilder( + start_executor=progress, + output_from=[finalizer], + intermediate_output_from=[progress], + ) + .add_edge(progress, finalizer) + .build() + ) + + sub = WorkflowExecutor(child, id="sub") + + class _ParentSink(Executor): + def __init__(self) -> None: + super().__init__(id="parent_sink") + self.received: list[str] = [] + + @handler + async def run(self, message: str, ctx: WorkflowContext[Never, str]) -> None: + self.received.append(message) + await ctx.yield_output(message) + + sink = _ParentSink() + parent = WorkflowBuilder(start_executor=sub, output_from=[sink]).add_edge(sub, sink).build() + + intermediate_events: list[WorkflowEvent[Any]] = [] + output_events: list[WorkflowEvent[Any]] = [] + async for event in parent.run("hello", stream=True): + if event.type == "intermediate": + intermediate_events.append(event) + elif event.type == "output": + output_events.append(event) + + # The child's intermediate emission bubbled up labeled with the WorkflowExecutor id, + # not the child's internal executor id. + assert len(intermediate_events) == 1, [(e.executor_id, e.data) for e in intermediate_events] + assert intermediate_events[0].executor_id == "sub" + assert intermediate_events[0].data == "progress: hello" + + # The parent's own terminal output is unaffected. + assert any(e.executor_id == "parent_sink" and e.data == "final: hello" for e in output_events) diff --git a/python/packages/core/tests/workflow/test_validation.py b/python/packages/core/tests/workflow/test_validation.py index be3c8b45f7..a9f62f35a3 100644 --- a/python/packages/core/tests/workflow/test_validation.py +++ b/python/packages/core/tests/workflow/test_validation.py @@ -550,12 +550,10 @@ def test_output_validation_with_valid_output_executors(): executor2 = OutputExecutor(id="executor2") # Build workflow with valid output executors - workflow = ( - WorkflowBuilder(start_executor=executor1, output_executors=[executor2]).add_edge(executor1, executor2).build() - ) + workflow = WorkflowBuilder(start_executor=executor1, output_from=[executor2]).add_edge(executor1, executor2).build() assert workflow is not None - assert workflow._output_executors == ["executor2"] # pyright: ignore[reportPrivateUsage] + assert {ex.id for ex in workflow.get_output_executors()} == {"executor2"} def test_output_validation_with_multiple_valid_output_executors(): @@ -565,14 +563,14 @@ def test_output_validation_with_multiple_valid_output_executors(): executor3 = OutputExecutor(id="executor3") workflow = ( - WorkflowBuilder(start_executor=executor1, output_executors=[executor1, executor3]) + WorkflowBuilder(start_executor=executor1, output_from=[executor1, executor3]) .add_edge(executor1, executor2) .add_edge(executor2, executor3) .build() ) assert workflow is not None - assert set(workflow._output_executors) == {"executor1", "executor3"} # pyright: ignore[reportPrivateUsage] + assert {ex.id for ex in workflow.get_output_executors()} == {"executor1", "executor3"} def test_output_validation_fails_for_nonexistent_executor(): @@ -598,7 +596,7 @@ def test_output_validation_fails_for_executor_without_output_types(): with pytest.raises(WorkflowValidationError) as exc_info: ( - WorkflowBuilder(start_executor=executor1, output_executors=[no_output_executor]) + WorkflowBuilder(start_executor=executor1, output_from=[no_output_executor]) .add_edge(executor1, no_output_executor) .build() ) @@ -608,16 +606,77 @@ def test_output_validation_fails_for_executor_without_output_types(): assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION -def test_output_validation_empty_list_passes(): - """Test that output validation passes with an empty output executors list.""" +def test_output_validation_empty_explicit_designation_fails(): + """Test that explicit mode rejects an empty output/intermediate designation.""" executor1 = OutputExecutor(id="executor1") executor2 = OutputExecutor(id="executor2") - workflow = WorkflowBuilder(start_executor=executor1, output_executors=[]).add_edge(executor1, executor2).build() + with pytest.raises(WorkflowValidationError) as exc_info: + WorkflowBuilder(start_executor=executor1, output_from=[]).add_edge(executor1, executor2).build() + + assert "at least one output or intermediate executor" in str(exc_info.value) + assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION + + +def test_output_validation_with_valid_intermediate_executors(): + """Test that output validation passes when intermediate executors exist and have output types.""" + executor1 = OutputExecutor(id="executor1") + executor2 = OutputExecutor(id="executor2") + + workflow = ( + WorkflowBuilder(start_executor=executor1, intermediate_output_from=[executor1]) + .add_edge(executor1, executor2) + .build() + ) assert workflow is not None - # All executors are outputs - assert workflow._output_executors == ["executor1", "executor2"] # type: ignore + assert {ex.id for ex in workflow.get_intermediate_executors()} == {"executor1"} + assert workflow.is_intermediate_executor("executor1") + assert not workflow.is_terminal_executor("executor2") + + +def test_output_validation_fails_for_designation_overlap(): + """Test that an executor cannot be both terminal and intermediate.""" + executor1 = OutputExecutor(id="executor1") + + with pytest.raises(WorkflowValidationError) as exc_info: + WorkflowBuilder( + start_executor=executor1, + output_from=[executor1], + intermediate_output_from=[executor1], + ).build() + + assert "both output and intermediate" in str(exc_info.value) + assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION + + +def test_output_validation_fails_for_duplicate_designation(): + """Test that duplicate output or intermediate designation entries are rejected.""" + executor1 = OutputExecutor(id="executor1") + + with pytest.raises(WorkflowValidationError) as exc_info: + WorkflowBuilder(start_executor=executor1, output_from=[executor1, executor1]).build() + + assert "Duplicate output executor designation" in str(exc_info.value) + assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION + + +def test_output_validation_fails_for_unknown_intermediate_executor(): + """Test that intermediate designation rejects executors outside the workflow graph.""" + executor1 = OutputExecutor(id="executor1") + executor2 = OutputExecutor(id="executor2") + missing = OutputExecutor(id="missing") + + with pytest.raises(WorkflowValidationError) as exc_info: + ( + WorkflowBuilder(start_executor=executor1, intermediate_output_from=[missing]) + .add_edge(executor1, executor2) + .build() + ) + + assert "not present in the workflow graph" in str(exc_info.value) + assert "missing" in str(exc_info.value) + assert exc_info.value.validation_type == ValidationTypeEnum.OUTPUT_VALIDATION def test_output_validation_with_direct_validate_workflow_graph(): diff --git a/python/packages/core/tests/workflow/test_workflow.py b/python/packages/core/tests/workflow/test_workflow.py index 30e81d8fe6..27f24d26f9 100644 --- a/python/packages/core/tests/workflow/test_workflow.py +++ b/python/packages/core/tests/workflow/test_workflow.py @@ -1056,7 +1056,7 @@ class PassthroughExecutor(Executor): async def test_output_executors_empty_yields_all_outputs() -> None: - """Test that when _output_executors is empty (default), all outputs are yielded.""" + """Test that omitted output selection yields all outputs for compatibility.""" # Create executors that each produce different outputs executor_a = PassthroughExecutor(id="executor_a", output_value=10) executor_b = OutputProducerExecutor(id="executor_b", output_value=20) @@ -1085,9 +1085,7 @@ async def test_output_executors_filters_outputs_non_streaming() -> None: # Build workflow with a -> b workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b]) - .add_edge(executor_a, executor_b) - .build() + WorkflowBuilder(start_executor=executor_a, output_from=[executor_b]).add_edge(executor_a, executor_b).build() ) result = await workflow.run(NumberMessage(data=0)) @@ -1110,9 +1108,7 @@ async def test_output_executors_filters_outputs_streaming() -> None: # Build workflow with a -> b workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a]) - .add_edge(executor_a, executor_b) - .build() + WorkflowBuilder(start_executor=executor_a, output_from=[executor_a]).add_edge(executor_a, executor_b).build() ) # Collect outputs from streaming @@ -1136,7 +1132,7 @@ async def test_output_executors_with_multiple_specified_executors() -> None: # Build workflow with a -> b -> c workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a, executor_c]) + WorkflowBuilder(start_executor=executor_a, output_from=[executor_a, executor_c]) .add_edge(executor_a, executor_b) .add_edge(executor_b, executor_c) .build() @@ -1154,12 +1150,15 @@ async def test_output_executors_with_multiple_specified_executors() -> None: async def test_output_executors_with_nonexistent_executor_id() -> None: """Test that specifying a non-existent executor ID doesn't break the workflow.""" + from agent_framework._workflows._workflow import OutputDesignation + executor_a = OutputProducerExecutor(id="executor_a", output_value=42) workflow = WorkflowBuilder(start_executor=executor_a).build() - # Set output_executors to an ID that doesn't exist - workflow._output_executors = ["nonexistent_executor"] # type: ignore + # Designate a nonexistent executor so the workflow-level filter drops every yield. + workflow._output_designation = OutputDesignation(outputs=frozenset({"nonexistent_executor"})) # type: ignore[attr-defined] + workflow._runner.context.set_yield_output_classifier(workflow._output_designation.classify) # type: ignore[attr-defined,reportPrivateUsage] result = await workflow.run(NumberMessage(data=0)) outputs = result.get_outputs() @@ -1199,7 +1198,7 @@ async def test_output_executors_filtering_with_fan_in() -> None: # Build fan-in workflow: start -> [a, b] -> aggregator workflow = ( - WorkflowBuilder(start_executor=executor_start, output_executors=[aggregator]) + WorkflowBuilder(start_executor=executor_start, output_from=[aggregator]) .add_fan_out_edges(executor_start, [executor_a, executor_b]) .add_fan_in_edges([executor_a, executor_b], aggregator) .build() @@ -1218,7 +1217,7 @@ async def test_output_executors_filtering_with_run_responses() -> None: """Test output filtering works correctly with run(responses=...) method.""" executor = MockExecutorRequestApproval(id="approval_executor") - workflow = WorkflowBuilder(start_executor=executor, output_executors=[executor]).build() + workflow = WorkflowBuilder(start_executor=executor, output_from=[executor]).build() # Run workflow which will request approval result = await workflow.run(NumberMessage(data=42)) @@ -1252,8 +1251,11 @@ async def test_output_executors_filtering_with_run_responses_streaming() -> None request_events = [e for e in events_list if e.type == "request_info"] assert len(request_events) == 1 - # Set output_executors to exclude the approval executor - workflow._output_executors = ["other_executor"] # type: ignore + # Designate a different executor so the workflow-level filter drops the approval yield. + from agent_framework._workflows._workflow import OutputDesignation + + workflow._output_designation = OutputDesignation(outputs=frozenset({"other_executor"})) # type: ignore[attr-defined] + workflow._runner.context.set_yield_output_classifier(workflow._output_designation.classify) # type: ignore[attr-defined,reportPrivateUsage] # Send approval response via streaming responses = {request_events[0].request_id: ApprovalMessage(approved=True)} diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index 0101a6e8a5..3dcdd26c86 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -923,7 +923,7 @@ class TestWorkflowAgent: # Build workflow: start -> agent1 (no output) -> agent2 (output visible) workflow = ( - WorkflowBuilder(start_executor=start_exec, output_executors=[start_exec, agent2]) + WorkflowBuilder(start_executor=start_exec, output_from=[start_exec, agent2]) .add_edge(start_exec, agent1) .add_edge(agent1, agent2) .build() diff --git a/python/packages/core/tests/workflow/test_workflow_agent_intermediate.py b/python/packages/core/tests/workflow/test_workflow_agent_intermediate.py new file mode 100644 index 0000000000..4fc66135f5 --- /dev/null +++ b/python/packages/core/tests/workflow/test_workflow_agent_intermediate.py @@ -0,0 +1,353 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for WorkflowAgent forwarding of intermediate workflow events. + +Covers: +- type='intermediate' surfaces as AgentResponseUpdate without content-type rewriting +- type='data' (compatibility alias via WorkflowEvent.emit) is forwarded +- Message.additional_properties survives the intermediate translation path +- Terminal yields keep using regular text content (backward compat) +""" + +from __future__ import annotations + +import warnings + +import pytest +from typing_extensions import Never + +from agent_framework import ( + AgentResponse, + AgentResponseUpdate, + Content, + Message, + WorkflowBuilder, + WorkflowContext, + WorkflowEvent, + executor, +) +from agent_framework.exceptions import AgentInvalidRequestException + + +@pytest.mark.asyncio +async def test_workflow_agent_forwards_intermediate_events_without_content_rewrite() -> None: + """An intermediate yield from an intermediate-designated executor surfaces through as_agent + as an AgentResponseUpdate carrying its original content type.""" + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output("intermediate progress") + await ctx.send_message("downstream") + + @executor + async def terminal(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("FINAL") + + workflow = ( + WorkflowBuilder( + start_executor=emit, + output_from=[terminal], + intermediate_output_from=[emit], + ) + .add_edge(emit, terminal) + .build() + ) + agent = workflow.as_agent("test") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hi", stream=True): + updates.append(update) + + text = " ".join(c.text for u in updates for c in u.contents if c.type == "text") + reasoning_text = " ".join(c.text for u in updates for c in u.contents if c.type == "text_reasoning") + + assert "intermediate progress" in text + assert "FINAL" in text + assert reasoning_text == "" + + +@pytest.mark.asyncio +async def test_workflow_agent_text_accessor_includes_forwarded_intermediate_text() -> None: + """Intermediate text is forwarded as text until issue 5885 defines the final mapping.""" + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output("invisible-progress") + await ctx.send_message("forward") + + @executor + async def terminal(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("the-answer") + + workflow = ( + WorkflowBuilder( + start_executor=emit, + output_from=[terminal], + intermediate_output_from=[emit], + ) + .add_edge(emit, terminal) + .build() + ) + agent = workflow.as_agent("test") + + response = await agent.run("hi") + assert isinstance(response, AgentResponse) + assert "invisible-progress" in response.text + assert "the-answer" in response.text + + +@pytest.mark.asyncio +async def test_workflow_agent_hidden_yields_do_not_surface_non_streaming() -> None: + """In explicit designation mode, unlisted executor yields stay out of agent responses.""" + + @executor + async def hidden(messages: list[Message], ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output("hidden-progress") + await ctx.send_message("forward") + + @executor + async def terminal(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("visible-answer") + + workflow = WorkflowBuilder(start_executor=hidden, output_from=[terminal]).add_edge(hidden, terminal).build() + agent = workflow.as_agent("test") + + response = await agent.run("hi") + all_text = " ".join(c.text for m in response.messages for c in m.contents if hasattr(c, "text")) + + assert response.text == "visible-answer" + assert "hidden-progress" not in all_text + + +@pytest.mark.asyncio +async def test_workflow_agent_hidden_yields_do_not_surface_streaming() -> None: + """In explicit designation mode, unlisted executor yields stay out of agent updates.""" + + @executor + async def hidden(messages: list[Message], ctx: WorkflowContext[str, str]) -> None: + await ctx.yield_output("hidden-progress") + await ctx.send_message("forward") + + @executor + async def terminal(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("visible-answer") + + workflow = WorkflowBuilder(start_executor=hidden, output_from=[terminal]).add_edge(hidden, terminal).build() + agent = workflow.as_agent("test") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hi", stream=True): + updates.append(update) + + all_text = " ".join(c.text for u in updates for c in u.contents if hasattr(c, "text")) + + assert "visible-answer" in all_text + assert "hidden-progress" not in all_text + + +@pytest.mark.asyncio +async def test_workflow_agent_data_event_emit_factory_still_forwarded() -> None: + """Even the deprecated WorkflowEvent.emit() / type='data' path is forwarded.""" + + @executor + async def emit_data_alias(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + await ctx.add_event(WorkflowEvent.emit("emit_data_alias", "data-alias-payload")) + await ctx.yield_output("DONE") + + workflow = WorkflowBuilder(start_executor=emit_data_alias, output_from=[emit_data_alias]).build() + agent = workflow.as_agent("test") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hi", stream=True): + updates.append(update) + + text = " ".join(c.text for u in updates for c in u.contents if c.type == "text") + assert "data-alias-payload" in text + + +@pytest.mark.asyncio +async def test_workflow_agent_intermediate_message_preserves_additional_properties() -> None: + """Message.additional_properties survives intermediate forwarding. + + Producer-attached metadata (tracking_id, conversation_id, etc.) must not disappear + for messages flowing through intermediate-designated executors. + """ + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[str, AgentResponse]) -> None: + msg = Message( + role="assistant", + contents=[Content.from_text(text="hi")], + additional_properties={"tracking_id": "abc-123"}, + ) + await ctx.yield_output(AgentResponse(messages=[msg])) + await ctx.send_message("forward") + + @executor + async def terminal(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("done") + + workflow = ( + WorkflowBuilder( + start_executor=emit, + output_from=[terminal], + intermediate_output_from=[emit], + ) + .add_edge(emit, terminal) + .build() + ) + agent = workflow.as_agent("test") + + response = await agent.run("hi") + intermediate_msgs = [m for m in response.messages if any(c.type == "text" and c.text == "hi" for c in m.contents)] + assert intermediate_msgs, "expected at least one intermediate message in the response" + assert intermediate_msgs[0].additional_properties.get("tracking_id") == "abc-123" + + +@pytest.mark.asyncio +async def test_workflow_agent_terminal_text_stays_text_not_reasoning() -> None: + """A designated executor's text yield surfaces as Content.text.""" + + @executor + async def only(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("the-answer") + + workflow = WorkflowBuilder(start_executor=only, output_from=[only]).build() + agent = workflow.as_agent("test") + + response = await agent.run("hi") + assert response.text == "the-answer" + # No text_reasoning content because everything from `only` is terminal. + assert all(c.type != "text_reasoning" for m in response.messages for c in m.contents) + + +@pytest.mark.asyncio +async def test_workflow_agent_non_streaming_rejects_terminal_update() -> None: + """A terminal event carrying AgentResponseUpdate is streaming-only and invalid in run().""" + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[Never, AgentResponseUpdate]) -> None: + await ctx.yield_output(AgentResponseUpdate(contents=[Content.from_text(text="partial")], role="assistant")) + + workflow = WorkflowBuilder(start_executor=emit, output_from=[emit]).build() + agent = workflow.as_agent("test") + + with pytest.raises(AgentInvalidRequestException, match="AgentResponseUpdate"): + await agent.run("hi") + + +@pytest.mark.asyncio +async def test_workflow_agent_non_streaming_rejects_intermediate_update() -> None: + """An intermediate event carrying AgentResponseUpdate is streaming-only and invalid in run().""" + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[str, AgentResponseUpdate]) -> None: + await ctx.yield_output(AgentResponseUpdate(contents=[Content.from_text(text="partial")], role="assistant")) + await ctx.send_message("forward") + + @executor + async def terminal(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output("FINAL") + + workflow = ( + WorkflowBuilder( + start_executor=emit, + output_from=[terminal], + intermediate_output_from=[emit], + ) + .add_edge(emit, terminal) + .build() + ) + agent = workflow.as_agent("test") + + with pytest.raises(AgentInvalidRequestException, match="AgentResponseUpdate"): + await agent.run("hi") + + +@pytest.mark.asyncio +async def test_workflow_agent_streaming_update_payloads_preserve_classification() -> None: + """Streaming AgentResponseUpdate payloads preserve original content types.""" + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[str, AgentResponseUpdate]) -> None: + await ctx.yield_output( + AgentResponseUpdate(contents=[Content.from_text(text="intermediate-chunk")], role="assistant") + ) + await ctx.send_message("forward") + + @executor + async def terminal(message: str, ctx: WorkflowContext[Never, AgentResponseUpdate]) -> None: + await ctx.yield_output( + AgentResponseUpdate(contents=[Content.from_text(text="terminal-chunk")], role="assistant") + ) + + workflow = ( + WorkflowBuilder( + start_executor=emit, + output_from=[terminal], + intermediate_output_from=[emit], + ) + .add_edge(emit, terminal) + .build() + ) + agent = workflow.as_agent("test") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hi", stream=True): + updates.append(update) + + text = " ".join(c.text for u in updates for c in u.contents if c.type == "text") + reasoning_text = " ".join(c.text for u in updates for c in u.contents if c.type == "text_reasoning") + + assert "intermediate-chunk" in text + assert "terminal-chunk" in text + assert reasoning_text == "" + + +@pytest.mark.asyncio +async def test_workflow_agent_drops_orchestration_internal_events() -> None: + """Orchestration-internal event types (group_chat / handoff_sent / magentic_orchestrator) + must not surface through workflow.as_agent(). Their dataclass payloads would otherwise + be stringified by the generic fallback path and leak into response history.""" + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: + # Construct typed orchestration-internal events directly to assert they get + # dropped at the agent boundary regardless of payload. + await ctx.add_event(WorkflowEvent("group_chat", data={"orchestrator": "details"})) # type: ignore[arg-type] + await ctx.add_event(WorkflowEvent("handoff_sent", data={"target": "agent_b"})) # type: ignore[arg-type] + await ctx.add_event(WorkflowEvent("magentic_orchestrator", data={"plan": "..."})) # type: ignore[arg-type] + await ctx.yield_output("FINAL") + + workflow = WorkflowBuilder(start_executor=emit, output_from=[emit]).build() + agent = workflow.as_agent("test") + + response = await agent.run("hi") + all_text = " ".join(c.text for m in response.messages for c in m.contents if hasattr(c, "text")) + assert "orchestrator" not in all_text + assert "agent_b" not in all_text + assert "plan" not in all_text + assert response.text == "FINAL" + + +@pytest.mark.asyncio +async def test_workflow_agent_drops_orchestration_internal_events_streaming() -> None: + """Streaming counterpart — orchestration-internal events stay inside the workflow.""" + + @executor + async def emit(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: + await ctx.add_event(WorkflowEvent("group_chat", data={"orchestrator": "details"})) # type: ignore[arg-type] + await ctx.yield_output("FINAL") + + workflow = WorkflowBuilder(start_executor=emit, output_from=[emit]).build() + agent = workflow.as_agent("test") + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("hi", stream=True): + updates.append(update) + + all_text = " ".join(c.text for u in updates for c in u.contents if hasattr(c, "text")) + assert "orchestrator" not in all_text + assert "FINAL" in all_text diff --git a/python/packages/core/tests/workflow/test_workflow_builder.py b/python/packages/core/tests/workflow/test_workflow_builder.py index c780bf4ac9..873d6e7c73 100644 --- a/python/packages/core/tests/workflow/test_workflow_builder.py +++ b/python/packages/core/tests/workflow/test_workflow_builder.py @@ -254,10 +254,10 @@ def test_switch_case_with_agents(): def test_with_output_from_returns_builder(): """Test that with_output_from returns the builder for method chaining.""" executor_a = MockExecutor(id="executor_a") - builder = WorkflowBuilder(output_executors=[executor_a], start_executor=executor_a) + builder = WorkflowBuilder(output_from=[executor_a], start_executor=executor_a) - # Verify builder was created with output_executors - assert builder._output_executors == [executor_a] # pyright: ignore[reportPrivateUsage] + # Verify builder was created with output_from + assert builder._output_from == [executor_a] # pyright: ignore[reportPrivateUsage] def test_with_output_from_with_executor_instances(): @@ -266,13 +266,11 @@ def test_with_output_from_with_executor_instances(): executor_b = MockExecutor(id="executor_b") workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b]) - .add_edge(executor_a, executor_b) - .build() + WorkflowBuilder(start_executor=executor_a, output_from=[executor_b]).add_edge(executor_a, executor_b).build() ) # Verify that the workflow was built with the correct output executors - assert workflow._output_executors == ["executor_b"] # type: ignore + assert {ex.id for ex in workflow.get_output_executors()} == {"executor_b"} def test_with_output_from_with_agent_instances(): @@ -280,10 +278,10 @@ def test_with_output_from_with_agent_instances(): agent_a = DummyAgent(id="agent_a", name="writer") agent_b = DummyAgent(id="agent_b", name="reviewer") - workflow = WorkflowBuilder(start_executor=agent_a, output_executors=[agent_b]).add_edge(agent_a, agent_b).build() + workflow = WorkflowBuilder(start_executor=agent_a, output_from=[agent_b]).add_edge(agent_a, agent_b).build() # Verify that the workflow was built with the agent's name as output executor - assert workflow._output_executors == ["reviewer"] # type: ignore + assert {ex.id for ex in workflow.get_output_executors()} == {"reviewer"} def test_with_output_from_with_executor_instances_by_id(): @@ -292,12 +290,10 @@ def test_with_output_from_with_executor_instances_by_id(): executor_b = MockExecutor(id="ExecutorB") workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b]) - .add_edge(executor_a, executor_b) - .build() + WorkflowBuilder(start_executor=executor_a, output_from=[executor_b]).add_edge(executor_a, executor_b).build() ) - assert workflow._output_executors == ["ExecutorB"] # type: ignore + assert {ex.id for ex in workflow.get_output_executors()} == {"ExecutorB"} def test_with_output_from_with_multiple_executors(): @@ -307,29 +303,27 @@ def test_with_output_from_with_multiple_executors(): executor_c = MockExecutor(id="executor_c") workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a, executor_c]) + WorkflowBuilder(start_executor=executor_a, output_from=[executor_a, executor_c]) .add_edge(executor_a, executor_b) .add_edge(executor_b, executor_c) .build() ) # Verify that the workflow was built with both output executors - assert set(workflow._output_executors) == {"executor_a", "executor_c"} # type: ignore + assert {ex.id for ex in workflow.get_output_executors()} == {"executor_a", "executor_c"} def test_with_output_from_can_be_set_to_different_value(): - """Test that output_executors can be set at construction time.""" + """Test that output_from can be set at construction time.""" executor_a = MockExecutor(id="executor_a") executor_b = MockExecutor(id="executor_b") workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b]) - .add_edge(executor_a, executor_b) - .build() + WorkflowBuilder(start_executor=executor_a, output_from=[executor_b]).add_edge(executor_a, executor_b).build() ) # Verify that the setting is applied - assert workflow._output_executors == ["executor_b"] # type: ignore + assert {ex.id for ex in workflow.get_output_executors()} == {"executor_b"} def test_with_output_from_with_agent_instances_resolves_name(): @@ -338,37 +332,37 @@ def test_with_output_from_with_agent_instances_resolves_name(): agent_reviewer = DummyAgent(id="agent2", name="reviewer") workflow = ( - WorkflowBuilder(start_executor=agent_writer, output_executors=[agent_reviewer]) + WorkflowBuilder(start_executor=agent_writer, output_from=[agent_reviewer]) .add_edge(agent_writer, agent_reviewer) .build() ) - assert workflow._output_executors == ["reviewer"] # type: ignore + assert {ex.id for ex in workflow.get_output_executors()} == {"reviewer"} def test_with_output_from_in_constructor(): - """Test that output_executors works correctly when set in the constructor.""" + """Test that output_from works correctly when set in the constructor.""" executor_a = MockExecutor(id="executor_a") executor_b = MockExecutor(id="executor_b") executor_c = MockExecutor(id="executor_c") - # Build workflow with output_executors in the constructor + # Build workflow with output_from in the constructor workflow = ( - WorkflowBuilder(start_executor=executor_a, output_executors=[executor_c]) + WorkflowBuilder(start_executor=executor_a, output_from=[executor_c]) .add_edge(executor_a, executor_b) .add_edge(executor_b, executor_c) .build() ) # Verify that the setting persists through the chain - assert workflow._output_executors == ["executor_c"] # type: ignore + assert {ex.id for ex in workflow.get_output_executors()} == {"executor_c"} def test_with_output_from_with_invalid_executor_raises_validation_error(): """Test that with_output_from with an invalid executor raises an error.""" executor_a = MockExecutor(id="executor_a") - builder = WorkflowBuilder(start_executor=executor_a, output_executors=[MockExecutor(id="executor_b")]) + builder = WorkflowBuilder(start_executor=executor_a, output_from=[MockExecutor(id="executor_b")]) # Attempting to set output from an executor not in the workflow should raise an error with pytest.raises( diff --git a/python/packages/core/tests/workflow/test_workflow_context.py b/python/packages/core/tests/workflow/test_workflow_context.py index a13c0b5a55..5889892435 100644 --- a/python/packages/core/tests/workflow/test_workflow_context.py +++ b/python/packages/core/tests/workflow/test_workflow_context.py @@ -5,6 +5,7 @@ from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import TYPE_CHECKING, Any +import pytest from typing_extensions import Never from agent_framework import ( @@ -72,6 +73,31 @@ async def test_executor_cannot_emit_framework_lifecycle_event(caplog: "LogCaptur assert any("attempted to emit" in message and "'status'" in message for message in list(caplog.messages)) +@pytest.mark.parametrize( + "event", + [ + WorkflowEvent("output", executor_id="exec", data="output-payload"), + WorkflowEvent("intermediate", executor_id="exec", data="intermediate-payload"), + ], +) +async def test_executor_cannot_emit_output_selection_events( + event: WorkflowEvent[Any], + caplog: "LogCaptureFixture", +) -> None: + async with make_context() as (ctx, runner_ctx): + caplog.clear() + with caplog.at_level("WARNING"): + await ctx.add_event(event) + + events: list[WorkflowEvent] = await runner_ctx.drain_events() + assert len(events) == 1 + assert events[0].type == "warning" + data = events[0].data + assert isinstance(data, str) + assert "reserved for ctx.yield_output()" in data + assert event.data not in [emitted.data for emitted in events] + + async def test_executor_emits_normal_event() -> None: async with make_context() as (ctx, runner_ctx): # Create a normal event to test event emission diff --git a/python/packages/core/tests/workflow/test_workflow_event_factories.py b/python/packages/core/tests/workflow/test_workflow_event_factories.py new file mode 100644 index 0000000000..ad24af693b --- /dev/null +++ b/python/packages/core/tests/workflow/test_workflow_event_factories.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for WorkflowEvent factory methods and WorkflowEvent.emit() deprecation.""" + +from __future__ import annotations + +import warnings + +import pytest + +from agent_framework import AgentResponse, Message +from agent_framework._workflows._events import WorkflowEvent + + +def test_workflow_event_output_selection_factories_are_not_public() -> None: + """Callers should use ctx.yield_output(), not direct output/intermediate factories.""" + assert not hasattr(WorkflowEvent, "output") + assert not hasattr(WorkflowEvent, "intermediate") + + +def test_workflow_event_emit_emits_deprecation_warning() -> None: + """Calling WorkflowEvent.emit() raises a DeprecationWarning recommending the new path.""" + response = AgentResponse(messages=[Message(role="assistant", contents=["x"])]) + with pytest.warns(DeprecationWarning, match="yield_output"): + WorkflowEvent.emit(executor_id="t", data=response) + + +def test_workflow_event_emit_still_returns_data_event() -> None: + """During the deprecation window, emit() still produces a type='data' event.""" + response = AgentResponse(messages=[Message(role="assistant", contents=["x"])]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + event = WorkflowEvent.emit(executor_id="t", data=response) + assert event.type == "data" diff --git a/python/packages/core/tests/workflow/test_workflow_kwargs.py b/python/packages/core/tests/workflow/test_workflow_kwargs.py index 9c664c6ac2..7bfa47a79f 100644 --- a/python/packages/core/tests/workflow/test_workflow_kwargs.py +++ b/python/packages/core/tests/workflow/test_workflow_kwargs.py @@ -377,7 +377,7 @@ async def test_kwargs_preserved_on_response_continuation() -> None: from agent_framework import WorkflowBuilder agent = _ApprovalCapturingAgent() - workflow = WorkflowBuilder(start_executor=agent, output_executors=[agent]).build() + workflow = WorkflowBuilder(start_executor=agent, output_from=[agent]).build() # Initial run with function_invocation_kwargs — workflow should pause for approval fi_kwargs = {"token": "abc"} diff --git a/python/packages/devui/agent_framework_devui/_mapper.py b/python/packages/devui/agent_framework_devui/_mapper.py index d4529875e5..f6a52ae945 100644 --- a/python/packages/devui/agent_framework_devui/_mapper.py +++ b/python/packages/devui/agent_framework_devui/_mapper.py @@ -72,6 +72,17 @@ def _stringify_name(value: Any) -> str: return value if isinstance(value, str) else str(value) +def _workflow_output_metadata(event_type: Any, executor_id: Any) -> dict[str, Any] | None: + """Return metadata that preserves workflow yield designation on visible output.""" + if event_type not in ("output", "intermediate", "data"): + return None + return { + "workflow_event_type": event_type, + "workflow_output_kind": "terminal" if event_type == "output" else "intermediate", + "executor_id": executor_id, + } + + def _serialize_content_recursive(value: Any) -> Any: """Recursively serialize Agent Framework Content objects to JSON-compatible values. @@ -200,15 +211,21 @@ class MessageMapper: try: from agent_framework import AgentResponse, AgentResponseUpdate, WorkflowEvent - # Handle WorkflowEvent with type='output' or 'data' wrapping AgentResponseUpdate - # This must be checked BEFORE generic WorkflowEvent check - # Note: AgentExecutor uses type='output' for streaming updates - if isinstance(raw_event, WorkflowEvent) and raw_event.type in ("output", "data"): + # Handle WorkflowEvent with type='output', 'intermediate', or 'data' wrapping + # AgentResponseUpdate. This must be checked BEFORE generic WorkflowEvent check. + # Note: AgentExecutor uses type='output' for streaming updates from designated + # executors and type='intermediate' from non-designated executors. type='data' + # is the deprecated legacy variant retained for backward compat. + if isinstance(raw_event, WorkflowEvent) and raw_event.type in ("output", "intermediate", "data"): event_data = getattr(cast(Any, raw_event), "data", None) if isinstance(event_data, AgentResponseUpdate): # Preserve executor_id in context for proper output routing context["current_executor_id"] = getattr(cast(Any, raw_event), "executor_id", None) - return await self._convert_agent_update(event_data, context) + context["current_workflow_event_type"] = raw_event.type + try: + return await self._convert_agent_update(event_data, context) + finally: + context.pop("current_workflow_event_type", None) # Handle complete agent response (AgentResponse) - for non-streaming agent execution if isinstance(raw_event, AgentResponse): @@ -633,6 +650,13 @@ class MessageMapper: # Check if we're in an executor context with an existing item executor_id = context.get("current_executor_id") executor_item_key = f"exec_item_{executor_id}" if executor_id else None + workflow_metadata = _workflow_output_metadata(context.get("current_workflow_event_type"), executor_id) + + if has_text_content and workflow_metadata is not None: + current_metadata = context.get("current_message_workflow_metadata") + if current_metadata != workflow_metadata: + context.pop("current_message_id", None) + context["current_message_workflow_metadata"] = workflow_metadata # If we have an executor item, use it for deltas instead of creating a message if has_text_content and executor_item_key and executor_item_key in context: @@ -644,6 +668,15 @@ class MessageMapper: message_id = f"msg_{uuid4().hex[:8]}" context["current_message_id"] = message_id context["output_index"] = context.get("output_index", -1) + 1 + message_item = ResponseOutputMessage( + type="message", + id=message_id, + role="assistant", + content=[], + status="in_progress", + ) + if workflow_metadata is not None: + cast(Any, message_item).metadata = workflow_metadata # Add message output item events.append( @@ -651,9 +684,7 @@ class MessageMapper: type="response.output_item.added", output_index=context["output_index"], sequence_number=self._next_sequence(context), - item=ResponseOutputMessage( - type="message", id=message_id, role="assistant", content=[], status="in_progress" - ), + item=message_item, ) ) @@ -675,17 +706,18 @@ class MessageMapper: # Special handling for TextContent to use proper delta events if content.type == "text" and "current_message_id" in context: # Stream text content via proper delta events - events.append( - ResponseTextDeltaEvent( - type="response.output_text.delta", - output_index=context["output_index"], - content_index=context.get("content_index", 0), - item_id=context["current_message_id"], - delta=content.text, - logprobs=[], # We don't have logprobs from Agent Framework - sequence_number=self._next_sequence(context), - ) + delta_event = ResponseTextDeltaEvent( + type="response.output_text.delta", + output_index=context["output_index"], + content_index=context.get("content_index", 0), + item_id=context["current_message_id"], + delta=content.text, + logprobs=[], # We don't have logprobs from Agent Framework + sequence_number=self._next_sequence(context), ) + if workflow_metadata is not None: + cast(Any, delta_event).metadata = workflow_metadata + events.append(delta_event) elif content.type in self.content_mappers: # Use existing mappers for other content types mapped_events = await self.content_mappers[content.type](content, context) @@ -899,10 +931,14 @@ class MessageMapper: return events - # Handle output events separately to preserve output data - if event_type == "output": + # Handle yield events (output / intermediate / data) by extracting visible + # text from the payload. All three render as a visible message item so the + # gap that previously dropped intermediate yields into generic completed- + # trace events is closed. + if event_type in ("output", "intermediate", "data"): output_data = getattr(event, "data", None) executor_id = getattr(event, "executor_id", "unknown") + workflow_metadata = _workflow_output_metadata(event_type, executor_id) if output_data is not None: # Import required types @@ -960,6 +996,8 @@ class MessageMapper: content=[text_content], status="completed", ) + if workflow_metadata is not None: + cast(Any, output_message).metadata = workflow_metadata # Emit output_item.added for each yield_output logger.debug( diff --git a/python/packages/devui/frontend/src/components/features/workflow/execution-timeline.tsx b/python/packages/devui/frontend/src/components/features/workflow/execution-timeline.tsx index b9b2fc7da4..2dca6d8a59 100644 --- a/python/packages/devui/frontend/src/components/features/workflow/execution-timeline.tsx +++ b/python/packages/devui/frontend/src/components/features/workflow/execution-timeline.tsx @@ -96,6 +96,14 @@ function getStateBadgeClass(state: ExecutorState) { } } +function getMessageText(item: unknown): string { + const content = (item as { content?: Array<{ type: string; text?: string }> }).content; + return content + ?.filter((content) => content.type === "output_text" && content.text) + .map((content) => content.text) + .join("\n") ?? ""; +} + function ExecutorRunItem({ run, isExpanded, @@ -282,7 +290,12 @@ export function ExecutionTimeline({ }); } else if (item && item.type === "message" && "metadata" in item && item.id) { // Handle message items from Magentic agents - const metadata = item.metadata as { agent_id?: string; source?: string } | undefined; + const metadata = item.metadata as { + agent_id?: string; + executor_id?: string; + source?: string; + workflow_output_kind?: string; + } | undefined; if (metadata?.agent_id && metadata?.source === "magentic") { const executorId = metadata.agent_id; const itemId = item.id; @@ -298,6 +311,21 @@ export function ExecutionTimeline({ timestamp: uiTimestamp, runNumber, }); + } else if (metadata?.executor_id && metadata.workflow_output_kind === "intermediate") { + const executorId = metadata.executor_id; + const itemId = item.id; + const runNumber = (runCount.get(executorId) || 0) + 1; + runCount.set(executorId, runNumber); + + runs.push({ + executorId, + executorName: truncateText(executorId, 35), + itemId, + state: item.status === "completed" ? "completed" : "running", + output: itemOutputs[itemId] || getMessageText(item), + timestamp: uiTimestamp, + runNumber, + }); } } } @@ -327,7 +355,12 @@ export function ExecutionTimeline({ } } else if (item && item.type === "message" && "metadata" in item && item.id) { // Handle message completion from Magentic agents - const metadata = item.metadata as { agent_id?: string; source?: string } | undefined; + const metadata = item.metadata as { + agent_id?: string; + executor_id?: string; + source?: string; + workflow_output_kind?: string; + } | undefined; if (metadata?.agent_id && metadata?.source === "magentic") { const itemId = item.id; const existingRun = runs.find((r) => r.itemId === itemId); @@ -336,6 +369,14 @@ export function ExecutionTimeline({ existingRun.state = item.status === "completed" ? "completed" : "failed"; existingRun.output = itemOutputs[itemId] || ""; } + } else if (metadata?.executor_id && metadata.workflow_output_kind === "intermediate") { + const itemId = item.id; + const existingRun = runs.find((r) => r.itemId === itemId); + + if (existingRun) { + existingRun.state = item.status === "completed" ? "completed" : "failed"; + existingRun.output = itemOutputs[itemId] || getMessageText(item); + } } } } diff --git a/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx b/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx index 4696ef57a5..7edeefb8ba 100644 --- a/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx +++ b/python/packages/devui/frontend/src/components/features/workflow/workflow-view.tsx @@ -663,6 +663,7 @@ export function WorkflowView({ item && item.type === "message" && (!("metadata" in item) || !(item.metadata as { source?: string } | undefined)?.source) && + (item.metadata as { workflow_output_kind?: string } | undefined)?.workflow_output_kind !== "intermediate" && "content" in item && Array.isArray(item.content) ) { @@ -1121,27 +1122,30 @@ export function WorkflowView({ // Handle workflow output messages if (item && item.type === "message" && "content" in item && Array.isArray(item.content)) { - // Extract text from message content - for (const content of item.content as Array<{ type: string; text?: string }>) { - if (content.type === "output_text" && content.text) { - const text = content.text; // Capture for closure - // Append to workflow result (support multiple yield_output calls) - setWorkflowResult((prev) => { - if (prev && prev.length > 0) { - // If there's existing output, add separator - return prev + "\n\n" + text; - } - return text; - }); + const metadata = item.metadata as { workflow_output_kind?: string } | undefined; + if (metadata?.workflow_output_kind !== "intermediate") { + // Extract text from message content + for (const content of item.content as Array<{ type: string; text?: string }>) { + if (content.type === "output_text" && content.text) { + const text = content.text; // Capture for closure + // Append to workflow result (support multiple yield_output calls) + setWorkflowResult((prev) => { + if (prev && prev.length > 0) { + // If there's existing output, add separator + return prev + "\n\n" + text; + } + return text; + }); - // Try to parse as JSON for structured metadata - try { - const parsed = JSON.parse(text); - if (typeof parsed === "object" && parsed !== null) { - workflowMetadata.current = parsed; + // Try to parse as JSON for structured metadata + try { + const parsed = JSON.parse(text); + if (typeof parsed === "object" && parsed !== null) { + workflowMetadata.current = parsed; + } + } catch { + // Not JSON, keep as text } - } catch { - // Not JSON, keep as text } } } diff --git a/python/packages/devui/frontend/src/types/openai.ts b/python/packages/devui/frontend/src/types/openai.ts index 31f7b20f02..9fba290027 100644 --- a/python/packages/devui/frontend/src/types/openai.ts +++ b/python/packages/devui/frontend/src/types/openai.ts @@ -376,6 +376,7 @@ export interface ResponseTextDeltaEvent extends ResponseStreamEvent { content_index: number; sequence_number: number; logprobs: Record[]; + metadata?: Record; } // OpenAI Response for non-streaming @@ -397,6 +398,7 @@ export interface ResponseOutputMessage { content: ResponseOutputText[]; id: string; status: "completed" | "failed" | "in_progress"; + metadata?: Record; } export interface ResponseOutputText { diff --git a/python/packages/devui/tests/devui/test_mapper.py b/python/packages/devui/tests/devui/test_mapper.py index b26900a89f..3ff4492d80 100644 --- a/python/packages/devui/tests/devui/test_mapper.py +++ b/python/packages/devui/tests/devui/test_mapper.py @@ -517,7 +517,8 @@ async def test_magentic_executor_event_with_agent_delta_metadata( """Test that WorkflowEvent[AgentResponseUpdate] with magentic_event_type='agent_delta' is handled correctly. This tests the ACTUAL event format Magentic emits - not a fake MagenticAgentDeltaEvent class. - Magentic uses WorkflowEvent.emit() with additional_properties containing magentic_event_type. + Magentic emits type='intermediate' WorkflowEvent instances with additional_properties + containing magentic_event_type. """ from agent_framework._types import AgentResponseUpdate from agent_framework._workflows._events import WorkflowEvent @@ -532,7 +533,7 @@ async def test_magentic_executor_event_with_agent_delta_metadata( "agent_id": "writer_agent", }, ) - event = WorkflowEvent.emit(executor_id="magentic_executor", data=update) + event = WorkflowEvent("intermediate", executor_id="magentic_executor", data=update) events = await mapper.convert_event(event, test_request) @@ -547,8 +548,8 @@ async def test_magentic_executor_event_with_agent_delta_metadata( async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None: """Test that WorkflowEvent[AgentResponseUpdate] with magentic_event_type='orchestrator_message' is handled. - Magentic emits orchestrator planning/instruction messages using WorkflowEvent.emit() - with additional_properties containing magentic_event_type='orchestrator_message'. + Magentic emits orchestrator planning/instruction messages using type='intermediate' + WorkflowEvent instances with additional_properties containing magentic_event_type='orchestrator_message'. """ from agent_framework._types import AgentResponseUpdate from agent_framework._workflows._events import WorkflowEvent @@ -564,7 +565,7 @@ async def test_magentic_orchestrator_message_event(mapper: MessageMapper, test_r "orchestrator_id": "magentic_orchestrator", }, ) - event = WorkflowEvent.emit(executor_id="magentic_orchestrator", data=update) + event = WorkflowEvent("intermediate", executor_id="magentic_orchestrator", data=update) events = await mapper.convert_event(event, test_request) @@ -595,7 +596,7 @@ async def test_magentic_events_use_same_event_class_as_other_workflows( contents=[Content.from_text(text="Regular workflow response")], role="assistant", ) - regular_event = WorkflowEvent.emit(executor_id="regular_executor", data=regular_update) + regular_event = WorkflowEvent("intermediate", executor_id="regular_executor", data=regular_update) # 2. Magentic workflow (with additional_properties) magentic_update = AgentResponseUpdate( @@ -603,7 +604,7 @@ async def test_magentic_events_use_same_event_class_as_other_workflows( role="assistant", additional_properties={"magentic_event_type": "agent_delta"}, ) - magentic_event = WorkflowEvent.emit(executor_id="magentic_executor", data=magentic_update) + magentic_event = WorkflowEvent("intermediate", executor_id="magentic_executor", data=magentic_update) # Both should be the SAME class assert type(regular_event) is type(magentic_event) @@ -653,7 +654,7 @@ async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentF """Test output event (type='output') is converted to output_item.added.""" from agent_framework._workflows._events import WorkflowEvent - event = WorkflowEvent.output(executor_id="final_executor", data="Final workflow output") + event = WorkflowEvent("output", executor_id="final_executor", data="Final workflow output") events = await mapper.convert_event(event, test_request) # output event (type='output') should emit output_item.added @@ -662,6 +663,9 @@ async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentF # Check item contains the output text item = events[0].item assert item.type == "message" + assert item.metadata["workflow_event_type"] == "output" + assert item.metadata["workflow_output_kind"] == "terminal" + assert item.metadata["executor_id"] == "final_executor" assert any("Final workflow output" in str(c) for c in item.content) @@ -675,13 +679,104 @@ async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_ Message(role="user", contents=[Content.from_text(text="Hello")]), Message(role="assistant", contents=[Content.from_text(text="World")]), ] - event = WorkflowEvent.output(executor_id="complete", data=messages) + event = WorkflowEvent("output", executor_id="complete", data=messages) events = await mapper.convert_event(event, test_request) assert len(events) == 1 assert events[0].type == "response.output_item.added" +async def test_workflow_intermediate_event_with_agent_response_update_dispatched( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """A WorkflowEvent with type='intermediate' wrapping an AgentResponseUpdate is mapped + just like type='output' / type='data' — to OpenAI text-delta events.""" + from agent_framework._workflows._events import WorkflowEvent + + update = AgentResponseUpdate( + contents=[Content.from_text(text="intermediate progress")], + role="assistant", + author_name="non-designated-agent", + ) + event = WorkflowEvent("intermediate", executor_id="non_designated", data=update) + events = await mapper.convert_event(event, test_request) + + assert len(events) >= 1 + added_events = [e for e in events if getattr(e, "type", "") == "response.output_item.added"] + assert added_events + item = added_events[0].item + assert item.metadata["workflow_event_type"] == "intermediate" + assert item.metadata["workflow_output_kind"] == "intermediate" + assert item.metadata["executor_id"] == "non_designated" + text_events = [e for e in events if getattr(e, "type", "") == "response.output_text.delta"] + assert len(text_events) >= 1 + assert text_events[0].metadata["workflow_event_type"] == "intermediate" + assert text_events[0].metadata["workflow_output_kind"] == "intermediate" + assert text_events[0].metadata["executor_id"] == "non_designated" + assert text_events[0].delta == "intermediate progress" + + +async def test_workflow_intermediate_event_with_string_payload_renders_visible_text( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """A WorkflowEvent with type='intermediate' wrapping a plain string surfaces as a + visible output item — not a generic completed-trace event. Without this, executors + that ``await ctx.yield_output("plan: …")`` from non-designated nodes are silently + dropped in DevUI.""" + from agent_framework._workflows._events import WorkflowEvent + + event = WorkflowEvent("intermediate", executor_id="planner", data="plan: starting work") + events = await mapper.convert_event(event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.output_item.added" + item = events[0].item + assert item.type == "message" + assert item.metadata["workflow_event_type"] == "intermediate" + assert item.metadata["workflow_output_kind"] == "intermediate" + assert item.metadata["executor_id"] == "planner" + assert any("plan: starting work" in str(c) for c in item.content) + + +async def test_workflow_intermediate_event_with_message_payload_renders_visible_text( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """type='intermediate' wrapping a Message surfaces visibly — same path as type='output'.""" + from agent_framework import Message + from agent_framework._workflows._events import WorkflowEvent + + msg = Message(role="assistant", contents=[Content.from_text(text="research note")]) + event = WorkflowEvent("intermediate", executor_id="researcher", data=msg) + events = await mapper.convert_event(event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.output_item.added" + item = events[0].item + assert item.metadata["workflow_event_type"] == "intermediate" + assert item.metadata["workflow_output_kind"] == "intermediate" + assert item.metadata["executor_id"] == "researcher" + assert any("research note" in str(c) for c in item.content) + + +async def test_workflow_data_event_keeps_intermediate_compatibility_metadata( + mapper: MessageMapper, test_request: AgentFrameworkRequest +) -> None: + """Deprecated type='data' workflow events remain visible and explicitly intermediate.""" + from agent_framework._workflows._events import WorkflowEvent + + with pytest.warns(DeprecationWarning): + event = WorkflowEvent.emit(executor_id="legacy", data="legacy progress") + events = await mapper.convert_event(event, test_request) + + assert len(events) == 1 + assert events[0].type == "response.output_item.added" + item = events[0].item + assert item.metadata["workflow_event_type"] == "data" + assert item.metadata["workflow_output_kind"] == "intermediate" + assert item.metadata["executor_id"] == "legacy" + assert any("legacy progress" in str(c) for c in item.content) + + # ============================================================================= # failed event (type='failed') Tests # ============================================================================= diff --git a/python/packages/foundry/tests/test_foundry_evals.py b/python/packages/foundry/tests/test_foundry_evals.py index d11999b76a..937a3cf524 100644 --- a/python/packages/foundry/tests/test_foundry_evals.py +++ b/python/packages/foundry/tests/test_foundry_evals.py @@ -1816,7 +1816,7 @@ class TestEvaluateWorkflow: WorkflowEvent.executor_completed("writer", [aer1]), WorkflowEvent.executor_invoked("reviewer", [aer1]), WorkflowEvent.executor_completed("reviewer", [aer2]), - WorkflowEvent.output("end", final_output), + WorkflowEvent("output", executor_id="end", data=final_output), ] wf_result = WorkflowRunResult(events, []) @@ -1845,7 +1845,7 @@ class TestEvaluateWorkflow: events = [ WorkflowEvent.executor_invoked("agent", "Test query"), WorkflowEvent.executor_completed("agent", [aer]), - WorkflowEvent.output("end", final_output), + WorkflowEvent("output", executor_id="end", data=final_output), ] wf_result = WorkflowRunResult(events, []) @@ -1875,7 +1875,7 @@ class TestEvaluateWorkflow: WorkflowEvent.executor_completed("input-conversation", None), WorkflowEvent.executor_invoked("planner", "Plan trip"), WorkflowEvent.executor_completed("planner", [aer]), - WorkflowEvent.output("end", final_output), + WorkflowEvent("output", executor_id="end", data=final_output), ] wf_result = WorkflowRunResult(events, []) @@ -1941,7 +1941,7 @@ class TestEvaluateWorkflow: WorkflowEvent.executor_completed("input-conversation", None), WorkflowEvent.executor_invoked("researcher", "What's the weather?"), WorkflowEvent.executor_completed("researcher", [aer]), - WorkflowEvent.output("end", [Message("assistant", ["Weather is sunny"])]), + WorkflowEvent("output", executor_id="end", data=[Message("assistant", ["Weather is sunny"])]), ] wf_result = WorkflowRunResult(events, []) @@ -2050,7 +2050,7 @@ class TestEvaluateWorkflow: events = [ WorkflowEvent.executor_invoked("agent", "Test query"), WorkflowEvent.executor_completed("agent", [aer]), - WorkflowEvent.output("end", final_output), + WorkflowEvent("output", executor_id="end", data=final_output), ] wf_result = WorkflowRunResult(events, []) @@ -2089,7 +2089,7 @@ class TestEvaluateWorkflow: events = [ WorkflowEvent.executor_invoked("agent", "Test query"), WorkflowEvent.executor_completed("agent", [aer]), - WorkflowEvent.output("end", final_output), + WorkflowEvent("output", executor_id="end", data=final_output), ] wf_result = WorkflowRunResult(events, []) diff --git a/python/packages/orchestrations/README.md b/python/packages/orchestrations/README.md index f965111712..63fd7ea4ee 100644 --- a/python/packages/orchestrations/README.md +++ b/python/packages/orchestrations/README.md @@ -18,6 +18,12 @@ Chain agents/executors in sequence, passing conversation context along: from agent_framework.orchestrations import SequentialBuilder workflow = SequentialBuilder(participants=[agent1, agent2, agent3]).build() + +# Preserve agent1 and agent2 as visible progress, while the default builder output remains Workflow Output. +workflow = SequentialBuilder( + participants=[agent1, agent2, agent3], + intermediate_output_from=[agent1, agent2], +).build() ``` ### ConcurrentBuilder @@ -55,6 +61,7 @@ from agent_framework.orchestrations import GroupChatBuilder workflow = GroupChatBuilder( participants=[agent1, agent2], selection_func=my_selector, + intermediate_output_from=[agent1, agent2], ).build() ``` @@ -68,9 +75,47 @@ from agent_framework.orchestrations import MagenticBuilder workflow = MagenticBuilder( participants=[researcher, writer, reviewer], manager_agent=manager_agent, + intermediate_output_from=[researcher, writer, reviewer], ).build() ``` +## Output Selection + +Orchestration builders expose Workflow Output selection using participant names. The core rule is that `output_from` +is an allow-list for Workflow Output, not a routing rule for every other participant output. Unselected participant +payloads are hidden unless `intermediate_output_from` explicitly selects them as Intermediate Output. + +- `output_from` designates participant emissions as Workflow Output (`type='output'` events). +- `intermediate_output_from` designates participant emissions as Intermediate Output (`type='intermediate'` events). + +If neither list is provided, each builder uses its documented default Workflow Output contract. Sequential emits the +last participant; Concurrent, GroupChat, and Magentic emit their aggregator/orchestrator/manager output; Handoff emits +participants. + +| Selection | Workflow Output | Intermediate Output | Hidden payloads | +| --- | --- | --- | --- | +| Omit both selections | Builder default Workflow Output contract | None | Builder-specific non-output participant payloads | +| `output_from="all"` | Every output-capable participant | None | None | +| `output_from=[writer]` | Only `writer` | None | All other participant payloads | +| `output_from=[writer], intermediate_output_from="all_other"` | Only `writer` | Every output-capable participant not selected by `output_from` | None | +| `intermediate_output_from="all_other"` | None, except builder-internal default output executors where applicable | Every output-capable participant | Builder-internal plumbing payloads | +| `output_from=[], intermediate_output_from="all_other"` | None, except builder-internal default output executors where applicable | Every output-capable participant | Builder-internal plumbing payloads | +| `output_from=[writer], intermediate_output_from=[researcher, reviewer]` | Only `writer` | `researcher` and `reviewer` | Any other participant payloads | + +Invalid selections fail at construction or build time: + +| Invalid selection | Why it fails | +| --- | --- | +| `output_from="all_other"` | `"all_other"` is only valid for `intermediate_output_from` | +| `intermediate_output_from="all"` | `"all"` is only valid for `output_from` | +| The same participant in both selections | One payload cannot be both Workflow Output and Intermediate Output | +| Duplicate participant selections | Duplicates are treated as configuration errors | +| Unknown participant selections | Typos and missing participants are rejected | +| `output_from=[], intermediate_output_from=[]` | Both explicit selections are empty | + +When an orchestration is wrapped with `workflow.as_agent()`, Workflow Output becomes normal response text. Intermediate +Output becomes `text_reasoning` content so callers can inspect progress without changing `.text` behavior. + ## Documentation For more information, see the [Agent Framework documentation](https://aka.ms/agent-framework). diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py index e1a931019a..6fc29c79b3 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_concurrent.py @@ -4,7 +4,7 @@ import asyncio import inspect import logging from collections.abc import Callable, Sequence -from typing import Any +from typing import Any, Literal, cast from agent_framework import AgentResponse, Message, SupportsAgentRun from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse @@ -18,6 +18,14 @@ from agent_framework._workflows._workflow_context import WorkflowContext from typing_extensions import Never from ._orchestration_request_info import AgentApprovalExecutor +from ._participant_output_config import ( + _MISSING, # pyright: ignore[reportPrivateUsage] + _coalesce_output_from, # pyright: ignore[reportPrivateUsage] + _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] + _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] + _ParticipantOutputSpecifier, # pyright: ignore[reportPrivateUsage] + _resolve_participant_output_config, # pyright: ignore[reportPrivateUsage] +) logger = logging.getLogger(__name__) @@ -205,23 +213,28 @@ class ConcurrentBuilder: *, participants: Sequence[SupportsAgentRun | Executor], checkpoint_storage: CheckpointStorage | None = None, - intermediate_outputs: bool = False, + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the ConcurrentBuilder. Args: participants: Sequence of agent or executor instances to run in parallel. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. - intermediate_outputs: If True, every participant's `yield_output` surfaces as a - workflow `output` event in addition to the aggregator's. By default - (False) only the aggregator's output surfaces. + output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``output`` events alongside the aggregator. Pass ``"all"`` to select every + participant. + intermediate_output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``intermediate`` events. Pass ``"all_other"`` to select every participant + not selected by ``output_from``. Unlisted participant outputs are hidden. """ self._participants: list[SupportsAgentRun | Executor] = [] self._aggregator: Executor | None = None self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage self._request_info_enabled: bool = False self._request_info_filter: set[str] | None = None - self._intermediate_outputs: bool = intermediate_outputs + self._output_from = _coalesce_output_from(output_from=output_from) + self._intermediate_output_from = _coerce_intermediate_output_from(intermediate_output_from) self._set_participants(participants) @@ -396,10 +409,19 @@ class ConcurrentBuilder: # Resolve participants and participant factories to executors participants: list[Executor] = self._resolve_participants() + # Default: only the aggregator is terminal; participant outputs are hidden + # unless explicitly designated as terminal or intermediate. + designated, intermediate_designated = _resolve_participant_output_config( + participants=participants, + output_from=self._output_from, + intermediate_output_from=self._intermediate_output_from, + extra_output_executors=[aggregator], + ) builder = WorkflowBuilder( start_executor=dispatcher, checkpoint_storage=self._checkpoint_storage, - output_executors=[aggregator] if not self._intermediate_outputs else None, + output_from=designated, + intermediate_output_from=intermediate_designated, ) # Fan-out for parallel execution builder.add_fan_out_edges(dispatcher, participants) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py index 9f7e011252..3778e5d110 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_group_chat.py @@ -27,7 +27,7 @@ import sys from collections import OrderedDict from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass -from typing import Any, ClassVar, cast +from typing import Any, ClassVar, Literal, cast from agent_framework import Agent, AgentResponse, AgentResponseUpdate, AgentSession, Message, SupportsAgentRun from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse @@ -51,6 +51,14 @@ from ._base_group_chat_orchestrator import ( ) from ._orchestration_request_info import AgentApprovalExecutor from ._orchestrator_helpers import clean_conversation_for_handoff +from ._participant_output_config import ( + _MISSING, # pyright: ignore[reportPrivateUsage] + _coalesce_output_from, # pyright: ignore[reportPrivateUsage] + _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] + _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] + _ParticipantOutputSpecifier, # pyright: ignore[reportPrivateUsage] + _resolve_participant_output_config, # pyright: ignore[reportPrivateUsage] +) if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover @@ -618,7 +626,8 @@ class GroupChatBuilder: termination_condition: TerminationCondition | None = None, max_rounds: int | None = None, checkpoint_storage: CheckpointStorage | None = None, - intermediate_outputs: bool = False, + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the GroupChatBuilder. @@ -635,9 +644,12 @@ class GroupChatBuilder: True to terminate the conversation, False to continue. max_rounds: Optional maximum number of orchestrator rounds to prevent infinite conversations. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. - intermediate_outputs: If True, every participant's `yield_output` surfaces as a - workflow `output` event in addition to the orchestrator's. By default (False) - only the orchestrator's output surfaces. + output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``output`` events alongside the orchestrator. Pass ``"all"`` to select every + participant. + intermediate_output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``intermediate`` events. Pass ``"all_other"`` to select every participant + not selected by ``output_from``. Unlisted participant outputs are hidden. """ self._participants: dict[str, SupportsAgentRun | Executor] = {} self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = [] @@ -658,7 +670,8 @@ class GroupChatBuilder: self._request_info_enabled: bool = False self._request_info_filter: set[str] = set() - self._intermediate_outputs: bool = intermediate_outputs + self._output_from = _coalesce_output_from(output_from=output_from) + self._intermediate_output_from = _coerce_intermediate_output_from(intermediate_output_from) if participants is None and participant_factories is None: raise ValueError("Either participants or participant_factories must be provided.") @@ -1001,11 +1014,20 @@ class GroupChatBuilder: participants: list[Executor] = self._resolve_participants() orchestrator: Executor = self._resolve_orchestrator(participants) - # Build workflow graph + # Default: only the orchestrator is terminal; participant outputs are hidden + # unless explicitly designated as terminal or intermediate. + # `group_chat` orchestrator-progress events keep their dedicated event type. + designated, intermediate_designated = _resolve_participant_output_config( + participants=participants, + output_from=self._output_from, + intermediate_output_from=self._intermediate_output_from, + extra_output_executors=[orchestrator], + ) workflow_builder = WorkflowBuilder( start_executor=orchestrator, checkpoint_storage=self._checkpoint_storage, - output_executors=[orchestrator] if not self._intermediate_outputs else None, + output_from=designated, + intermediate_output_from=intermediate_designated, ) for participant in participants: # Orchestrator and participant bi-directional edges diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py index f555ab89b0..70f28e7f04 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py @@ -36,7 +36,7 @@ import sys from collections.abc import Awaitable, Callable, Mapping, Sequence from copy import deepcopy from dataclasses import dataclass -from typing import Any +from typing import Any, Literal, cast from agent_framework import Agent, AgentResponse, Message, SupportsAgentRun from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware, MiddlewareTermination @@ -53,6 +53,14 @@ from agent_framework._workflows._workflow_context import WorkflowContext from ._base_group_chat_orchestrator import TerminationCondition from ._orchestrator_helpers import clean_conversation_for_handoff +from ._participant_output_config import ( + _MISSING, # pyright: ignore[reportPrivateUsage] + _coalesce_output_from, # pyright: ignore[reportPrivateUsage] + _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] + _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] + _ParticipantOutputSpecifier, # pyright: ignore[reportPrivateUsage] + _resolve_participant_output_config, # pyright: ignore[reportPrivateUsage] +) if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover @@ -377,7 +385,7 @@ class HandoffAgentExecutor(AgentExecutor): # Append the agent response to the full conversation history. This list removes # function call related content such that the result stays consistent regardless - # of which agent yields the final output. + # of which agent yields Workflow Output. self._full_conversation.extend(cleaned_response) # Broadcast only the cleaned response to other agents (without function_calls/results) @@ -577,7 +585,7 @@ class HandoffBuilder: Note: 1. Agents in handoff workflows must be ``Agent`` instances and support local tool calls. - 2. Because each agent's response is itself a workflow output, handoff has no separate + 2. Because each agent's response is itself Workflow Output, handoff has no separate "intermediate outputs" channel — every per-agent response is the primary output. """ @@ -589,6 +597,8 @@ class HandoffBuilder: description: str | None = None, checkpoint_storage: CheckpointStorage | None = None, termination_condition: TerminationCondition | None = None, + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: r"""Initialize a HandoffBuilder for creating conversational handoff workflows. @@ -610,6 +620,12 @@ class HandoffBuilder: checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. termination_condition: Optional callable that receives the full conversation and returns True (or awaitable True) if the workflow should terminate. + output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``output`` events. Defaults to all participants; pass ``"all"`` to select every + participant explicitly. + intermediate_output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``intermediate`` events. Pass ``"all_other"`` to select every participant + not selected by ``output_from``. Unlisted participant outputs are hidden. """ self._name = name self._description = description @@ -635,6 +651,8 @@ class HandoffBuilder: # Termination related members self._termination_condition: Callable[[list[Message]], bool | Awaitable[bool]] | None = termination_condition + self._output_from = _coalesce_output_from(output_from=output_from) + self._intermediate_output_from = _coerce_intermediate_output_from(intermediate_output_from) def participants(self, participants: Sequence[Agent]) -> "HandoffBuilder": """Register the agents that will participate in the handoff workflow. @@ -955,11 +973,22 @@ class HandoffBuilder: if self._start_id is None: raise ValueError("Must call with_start_agent(...) before building the workflow.") start_executor = executors[self._resolve_to_id(resolved_agents[self._start_id])] + # Handoff has no separate terminator: every participant's reply is a primary + # output by default. Explicit participant designation can narrow or reclassify + # selected speakers. + output, intermediate_output = _resolve_participant_output_config( + participants=list(executors.values()), + output_from=self._output_from, + intermediate_output_from=self._intermediate_output_from, + default_output_from=list(executors.values()), + ) builder = WorkflowBuilder( name=self._name, description=self._description, start_executor=start_executor, checkpoint_storage=self._checkpoint_storage, + output_from=output, + intermediate_output_from=intermediate_output, ) # Add the appropriate edges diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py b/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py index 7f1854f914..53ca4052ff 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_magentic.py @@ -10,7 +10,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Sequence from dataclasses import dataclass, field from enum import Enum -from typing import Any, ClassVar, TypeVar, cast +from typing import Any, ClassVar, Literal, TypeVar, cast from agent_framework import ( AgentResponse, @@ -38,6 +38,14 @@ from ._base_group_chat_orchestrator import ( GroupChatWorkflowContextOutT, ParticipantRegistry, ) +from ._participant_output_config import ( + _MISSING, # pyright: ignore[reportPrivateUsage] + _coalesce_output_from, # pyright: ignore[reportPrivateUsage] + _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] + _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] + _ParticipantOutputSpecifier, # pyright: ignore[reportPrivateUsage] + _resolve_participant_output_config, # pyright: ignore[reportPrivateUsage] +) if sys.version_info >= (3, 12): from typing import override # type: ignore # pragma: no cover @@ -1409,7 +1417,8 @@ class MagenticBuilder: # Existing params enable_plan_review: bool = False, checkpoint_storage: CheckpointStorage | None = None, - intermediate_outputs: bool = False, + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the Magentic workflow builder. @@ -1432,9 +1441,12 @@ class MagenticBuilder: max_round_count: Max total coordination rounds. None means unlimited. enable_plan_review: If True, requires human approval of the initial plan before proceeding. checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence. - intermediate_outputs: If True, every participant's `yield_output` surfaces as a - workflow `output` event in addition to the orchestrator's. By default (False) - only the orchestrator's output surfaces. + output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``output`` events alongside the manager. Pass ``"all"`` to select every + participant. + intermediate_output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``intermediate`` events. Pass ``"all_other"`` to select every participant + not selected by ``output_from``. Unlisted participant outputs are hidden. """ self._participants: dict[str, SupportsAgentRun | Executor] = {} @@ -1447,7 +1459,8 @@ class MagenticBuilder: self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage - self._intermediate_outputs = intermediate_outputs + self._output_from = _coalesce_output_from(output_from=output_from) + self._intermediate_output_from = _coerce_intermediate_output_from(intermediate_output_from) self._set_participants(participants) @@ -1762,11 +1775,20 @@ class MagenticBuilder: participants: list[Executor] = self._resolve_participants() orchestrator: Executor = self._resolve_orchestrator(participants) - # Build workflow graph + # Default: only the manager is terminal; worker outputs are hidden unless + # explicitly designated as terminal or intermediate. + # `magentic_orchestrator` events keep their dedicated event type. + designated, intermediate_designated = _resolve_participant_output_config( + participants=participants, + output_from=self._output_from, + intermediate_output_from=self._intermediate_output_from, + extra_output_executors=[orchestrator], + ) workflow_builder = WorkflowBuilder( start_executor=orchestrator, checkpoint_storage=self._checkpoint_storage, - output_executors=[orchestrator] if not self._intermediate_outputs else None, + output_from=designated, + intermediate_output_from=intermediate_designated, ) for participant in participants: # Orchestrator and participant bi-directional edges diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py index 23e382aa13..66949ae576 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_orchestration_request_info.py @@ -220,8 +220,14 @@ class AgentApprovalExecutor(WorkflowExecutor): request_info_cls = _TerminalAgentRequestInfoExecutor if terminal else AgentRequestInfoExecutor request_info_executor = request_info_cls(id="agent_request_info_executor") + # Both inner executors yield the inner workflow's terminal output (the agent + # during its turn; the _TerminalAgentRequestInfoExecutor after approval), so + # both must be designated for WorkflowExecutor.get_outputs() to surface them. return ( - WorkflowBuilder(start_executor=agent_executor) + WorkflowBuilder( + start_executor=agent_executor, + output_from=[agent_executor, request_info_executor], + ) # Create a loop between agent executor and request info executor .add_edge(agent_executor, request_info_executor) .add_edge(request_info_executor, agent_executor) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py b/python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py new file mode 100644 index 0000000000..49138b7d0d --- /dev/null +++ b/python/packages/orchestrations/agent_framework_orchestrations/_participant_output_config.py @@ -0,0 +1,166 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Participant-oriented workflow output configuration helpers.""" + +from collections.abc import Sequence +from typing import Any, Literal + +from agent_framework import SupportsAgentRun +from agent_framework._workflows._agent_utils import resolve_agent_id +from agent_framework._workflows._executor import Executor + +_MISSING = object() +_ALL_OUTPUTS: Literal["all"] = "all" +_ALL_OTHER_OUTPUTS: Literal["all_other"] = "all_other" +_ParticipantOutputSpecifier = str | SupportsAgentRun | Executor +_ParticipantOutputSelection = Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None +_ParticipantIntermediateOutputSelection = Sequence[_ParticipantOutputSpecifier] | Literal["all", "all_other"] | None +_WorkflowExecutorSpecifier = Executor | SupportsAgentRun + + +def _coalesce_output_from( # pyright: ignore[reportUnusedFunction] + *, + output_from: Any = _MISSING, +) -> _ParticipantOutputSelection: + """Resolve orchestration output selection to ``output_from``.""" + if output_from is not _MISSING: + return _coerce_output_from(output_from) + return None + + +def _coerce_output_from(output_from: Any) -> _ParticipantOutputSelection: + """Coerce workflow-output participant selection while preserving the ``"all"`` literal.""" + if output_from is None: + return None + if isinstance(output_from, str): + if output_from == _ALL_OUTPUTS: + return _ALL_OUTPUTS + if output_from == _ALL_OTHER_OUTPUTS: + raise ValueError("output_from='all_other' is invalid; use intermediate_output_from='all_other' instead.") + raise ValueError(f"Unsupported output_from literal {output_from!r}; use 'all' or a list of participants.") + return list(output_from) + + +def _coerce_intermediate_output_from( # pyright: ignore[reportUnusedFunction] + intermediate_output_from: Any, +) -> _ParticipantIntermediateOutputSelection: + """Coerce intermediate-output participant selection while preserving ``"all_other"``.""" + if intermediate_output_from is None: + return None + if isinstance(intermediate_output_from, str): + if intermediate_output_from == _ALL_OUTPUTS: + return _ALL_OUTPUTS + if intermediate_output_from == _ALL_OTHER_OUTPUTS: + return _ALL_OTHER_OUTPUTS + raise ValueError( + f"Unsupported intermediate_output_from literal {intermediate_output_from!r}; " + "use 'all', 'all_other', or a list of participants." + ) + return list(intermediate_output_from) + + +def _resolve_participant_output_config( # pyright: ignore[reportUnusedFunction] + *, + participants: Sequence[Executor], + output_from: _ParticipantOutputSelection, + intermediate_output_from: _ParticipantIntermediateOutputSelection, + default_output_from: Sequence[Executor] = (), + extra_output_executors: Sequence[Executor] = (), +) -> tuple[list[_WorkflowExecutorSpecifier], list[_WorkflowExecutorSpecifier]]: + """Resolve public participant output config into workflow executor config.""" + explicit_config = output_from is not None or intermediate_output_from is not None + if explicit_config and not (output_from or intermediate_output_from): + raise ValueError("output_from and intermediate_output_from cannot both be empty.") + + participants_by_id = {participant.id: participant for participant in participants} + known_participants = sorted(participants_by_id) + + if output_from == _ALL_OUTPUTS: + output_designated = list(participants) + elif output_from is not None: + output_designated = _resolve_designated_participants( + output_from, + kind="output", + participants_by_id=participants_by_id, + known_participants=known_participants, + ) + elif intermediate_output_from in (_ALL_OTHER_OUTPUTS, _ALL_OUTPUTS): + output_designated = [] + else: + intermediate_designated = ( + _resolve_designated_participants( + intermediate_output_from, + kind="intermediate", + participants_by_id=participants_by_id, + known_participants=known_participants, + ) + if intermediate_output_from is not None + else [] + ) + # The caller-supplied default applies only to participants not explicitly designated as + # intermediate. Without this subtraction, builders that pre-populate a default output list + # (Handoff defaults to all participants, Sequential defaults to the last) would force + # an overlap error whenever a user passed `intermediate_output_from=[X]` for an X in + # the default set, contradicting the public docstring contract. + intermediate_ids = {participant.id for participant in intermediate_designated} + output_designated = [ + participant for participant in default_output_from if participant.id not in intermediate_ids + ] + + if intermediate_output_from == _ALL_OUTPUTS: + intermediate_designated = list(participants) + elif intermediate_output_from == _ALL_OTHER_OUTPUTS: + output_ids = {participant.id for participant in output_designated} + intermediate_designated = [participant for participant in participants if participant.id not in output_ids] + elif intermediate_output_from is not None: + intermediate_designated = _resolve_designated_participants( + intermediate_output_from, + kind="intermediate", + participants_by_id=participants_by_id, + known_participants=known_participants, + ) + else: + intermediate_designated = [] + + overlap = sorted( + {participant.id for participant in output_designated}.intersection( + participant.id for participant in intermediate_designated + ) + ) + if overlap: + raise ValueError(f"Participants cannot be both output and intermediate designated: {overlap}") + + output_executors: list[_WorkflowExecutorSpecifier] = [*extra_output_executors, *output_designated] + intermediate_executors: list[_WorkflowExecutorSpecifier] = list(intermediate_designated) + return output_executors, intermediate_executors + + +def _resolve_designated_participants( + designations: Sequence[_ParticipantOutputSpecifier], + *, + kind: str, + participants_by_id: dict[str, Executor], + known_participants: Sequence[str], +) -> list[Executor]: + resolved: list[Executor] = [] + seen: set[str] = set() + for designation in designations: + participant_id = _participant_id(designation) + if participant_id in seen: + raise ValueError(f"Duplicate {kind} participant '{participant_id}' in {kind}_participants.") + seen.add(participant_id) + try: + resolved.append(participants_by_id[participant_id]) + except KeyError as exc: + raise ValueError( + f"Unknown {kind} participant '{participant_id}'. Known participants: {known_participants}" + ) from exc + return resolved + + +def _participant_id(participant: _ParticipantOutputSpecifier) -> str: + if isinstance(participant, str): + return participant + if isinstance(participant, Executor): + return participant.id + return resolve_agent_id(participant) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py index 36d4f23f49..70796d5e26 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_sequential.py @@ -16,7 +16,7 @@ produces — by convention an `AgentResponse` so downstream consumers see a unif import logging from collections.abc import Sequence -from typing import Literal +from typing import Any, Literal, cast from agent_framework import Message, SupportsAgentRun from agent_framework._workflows._agent_executor import AgentExecutor @@ -32,6 +32,14 @@ from agent_framework._workflows._workflow_builder import WorkflowBuilder from agent_framework._workflows._workflow_context import WorkflowContext from ._orchestration_request_info import AgentApprovalExecutor +from ._participant_output_config import ( + _MISSING, # pyright: ignore[reportPrivateUsage] + _coalesce_output_from, # pyright: ignore[reportPrivateUsage] + _coerce_intermediate_output_from, # pyright: ignore[reportPrivateUsage] + _ParticipantIntermediateOutputSelection, # pyright: ignore[reportPrivateUsage] + _ParticipantOutputSpecifier, # pyright: ignore[reportPrivateUsage] + _resolve_participant_output_config, # pyright: ignore[reportPrivateUsage] +) logger = logging.getLogger(__name__) @@ -60,7 +68,7 @@ class SequentialBuilder: - The workflow wires participants in order, passing a list[Message] down the chain - Agents append their assistant messages to the conversation - Custom executors can transform/summarize and return a list[Message] - - The final output is the conversation produced by the last participant + - The default Workflow Output is the conversation produced by the last participant Usage: @@ -91,7 +99,8 @@ class SequentialBuilder: participants: Sequence[SupportsAgentRun | Executor], checkpoint_storage: CheckpointStorage | None = None, chain_only_agent_responses: bool = False, - intermediate_outputs: bool = False, + output_from: Sequence[_ParticipantOutputSpecifier] | Literal["all"] | None = cast(Any, _MISSING), + intermediate_output_from: _ParticipantIntermediateOutputSelection = None, ) -> None: """Initialize the SequentialBuilder. @@ -101,16 +110,19 @@ class SequentialBuilder: chain_only_agent_responses: If True, only agent responses are chained between agents. By default, the full conversation context is passed to the next agent. This also applies to Executor -> Agent transitions if the executor sends `AgentExecutorResponse`. - intermediate_outputs: If True, every participant's `yield_output` surfaces as a - workflow `output` event in addition to the terminator's. By default (False) only - the last participant's output surfaces. + output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``output`` events. Pass ``"all"`` to select every participant. + intermediate_output_from: Optional participant names or instances whose ``yield_output`` calls + surface as workflow ``intermediate`` events. Pass ``"all_other"`` to select every participant + not selected by ``output_from``. Unlisted participant outputs are hidden. """ self._participants: list[SupportsAgentRun | Executor] = [] self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage self._chain_only_agent_responses: bool = chain_only_agent_responses self._request_info_enabled: bool = False self._request_info_filter: set[str] | None = None - self._intermediate_outputs: bool = intermediate_outputs + self._output_from = _coalesce_output_from(output_from=output_from) + self._intermediate_output_from = _coerce_intermediate_output_from(intermediate_output_from) self._set_participants(participants) @@ -225,8 +237,8 @@ class SequentialBuilder: - Custom `Executor`: receives `list[Message]` and forwards `list[Message]`. If used as the terminator, it must call `ctx.yield_output(AgentResponse(...))` instead of `ctx.send_message(...)` — its yield becomes the workflow's output. - - The last participant is registered as the workflow's `output_executor`, so the - terminator's own `yield_output` is the workflow's terminal output (`AgentResponse`, + - The last participant is selected as Workflow Output by default, so the + terminator's own `yield_output` is Workflow Output (`AgentResponse`, or per-chunk `AgentResponseUpdate` when streaming). """ input_conv = _InputToConversation(id="input-conversation") @@ -234,10 +246,19 @@ class SequentialBuilder: # Resolve participants and participant factories to executors participants: list[Executor] = self._resolve_participants() + # Default: only the terminator is terminal. Explicit participant designation + # can surface selected earlier participant outputs as terminal or intermediate. + designated, intermediate_designated = _resolve_participant_output_config( + participants=participants, + output_from=self._output_from, + intermediate_output_from=self._intermediate_output_from, + default_output_from=[participants[-1]], + ) builder = WorkflowBuilder( start_executor=input_conv, checkpoint_storage=self._checkpoint_storage, - output_executors=[participants[-1]] if not self._intermediate_outputs else None, + output_from=designated, + intermediate_output_from=intermediate_designated, ) prior: Executor | SupportsAgentRun = input_conv diff --git a/python/packages/orchestrations/tests/test_magentic.py b/python/packages/orchestrations/tests/test_magentic.py index 0389fad94e..5c94d2fb14 100644 --- a/python/packages/orchestrations/tests/test_magentic.py +++ b/python/packages/orchestrations/tests/test_magentic.py @@ -630,9 +630,14 @@ class StubAssistantsAgent(BaseAgent): async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[Message]: captured: list[Message] = [] - wf = MagenticBuilder(participants=[participant], intermediate_outputs=True, manager=InvokeOnceManager()).build() + wf = MagenticBuilder( + participants=[participant], + output_from=[participant], + manager=InvokeOnceManager(), + ).build() - # Run a bounded stream to allow one invoke and then completion + # With output_from, participants are designated as outputs alongside + # the manager — so their streaming chunks surface as type='output' (not intermediate). events: list[WorkflowEvent] = [] async for ev in wf.run("task", stream=True): events.append(ev) diff --git a/python/packages/orchestrations/tests/test_orchestration_intermediate_vs_terminal.py b/python/packages/orchestrations/tests/test_orchestration_intermediate_vs_terminal.py new file mode 100644 index 0000000000..78086d9d41 --- /dev/null +++ b/python/packages/orchestrations/tests/test_orchestration_intermediate_vs_terminal.py @@ -0,0 +1,749 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for orchestration intermediate vs terminal output labeling. + +Verifies that under the strict-output model: + - Sequential / Concurrent / GroupChat / Magentic designate their terminator, + aggregator, orchestrator, or manager as the sole output executor; per-step + yields from non-designated executors emit `type='intermediate'` events. + - Handoff designates ALL participants — every reply is `type='output'`. + - When wrapped via `workflow.as_agent()`, caller-facing workflow events surface + with their original content types. +""" + +from __future__ import annotations + +from collections.abc import AsyncIterable, Awaitable, Callable +from typing import Any, ClassVar, Literal, overload + +import pytest +from agent_framework import ( + AgentResponse, + AgentResponseUpdate, + AgentRunInputs, + AgentSession, + BaseAgent, + Content, + Message, + ResponseStream, +) +from agent_framework.orchestrations import ( + ConcurrentBuilder, + GroupChatBuilder, + GroupChatState, + HandoffBuilder, + MagenticBuilder, + MagenticContext, + MagenticManagerBase, + MagenticProgressLedger, + MagenticProgressLedgerItem, + SequentialBuilder, +) + + +class _EchoAgent(BaseAgent): + """Minimal non-streaming agent that returns a single assistant message.""" + + @overload + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[False] = ..., + session: AgentSession | None = ..., + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]]: ... + @overload + def run( + self, + messages: AgentRunInputs | None = ..., + *, + stream: Literal[True], + session: AgentSession | None = ..., + **kwargs: Any, + ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... + + def run( + self, + messages: AgentRunInputs | None = None, + *, + stream: bool = False, + session: AgentSession | None = None, + **kwargs: Any, + ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: + if stream: + + async def _stream() -> AsyncIterable[AgentResponseUpdate]: + yield AgentResponseUpdate( + contents=[Content.from_text(text=f"{self.name} reply")], author_name=self.name + ) + + return ResponseStream(_stream(), finalizer=AgentResponse.from_updates) + + async def _run() -> AgentResponse: + return AgentResponse(messages=[Message("assistant", [f"{self.name} reply"], author_name=self.name)]) + + return _run() + + +# --------------------------------------------------------------------------- +# Sequential +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_sequential_default_only_terminator_is_output() -> None: + """Default Sequential designates only the terminator; earlier participants are hidden.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c]).build() + + output_events: list[Any] = [] + intermediate_events: list[Any] = [] + async for event in workflow.run("hello", stream=True): + if event.type == "output": + output_events.append(event) + elif event.type == "intermediate": + intermediate_events.append(event) + + # Only the terminator (C) emits type='output'. + assert len(output_events) == 1 + assert "C" in {ev.executor_id for ev in output_events} + + assert not intermediate_events + + +@pytest.mark.asyncio +async def test_sequential_output_from_designates_workflow_output_participants() -> None: + """Sequential output_from controls which participant yields surface as workflow output.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c], output_from=["A", "B", "C"]).build() + result = await workflow.run("hello") + outputs = result.get_outputs() + assert len(outputs) == 3 + + +@pytest.mark.asyncio +async def test_sequential_intermediate_output_from_surface_as_intermediate() -> None: + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c], intermediate_output_from=[a, "B"]).build() + + output_executors: set[str] = set() + intermediate_executors: set[str] = set() + async for event in workflow.run("hello", stream=True): + if event.type == "output" and event.executor_id is not None: + output_executors.add(event.executor_id) + elif event.type == "intermediate" and event.executor_id is not None: + intermediate_executors.add(event.executor_id) + + assert output_executors == {"C"} + assert intermediate_executors == {"A", "B"} + + +@pytest.mark.asyncio +async def test_sequential_intermediate_can_demote_default_terminator() -> None: + """Regression: marking the default output terminator as intermediate must not raise an overlap error. + + Sequential's default output list is `[participants[-1]]`. Before the fix, designating that + same participant via `intermediate_output_from` triggered the + "Participants cannot be both output and intermediate designated" overlap rejection in + `_participant_output_config`, contradicting the public contract that + `intermediate_output_from` can be used independently of `output_from`. + """ + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c], intermediate_output_from=["C"]).build() + + output_executors: set[str] = set() + intermediate_executors: set[str] = set() + async for event in workflow.run("hello", stream=True): + if event.type == "output" and event.executor_id is not None: + output_executors.add(event.executor_id) + elif event.type == "intermediate" and event.executor_id is not None: + intermediate_executors.add(event.executor_id) + + # The default-final list ([C]) is implicitly narrowed by the intermediate designation, + # so no participant surfaces as terminal output and C surfaces as intermediate. + assert output_executors == set() + assert intermediate_executors == {"C"} + + +@pytest.mark.asyncio +async def test_sequential_get_outputs_returns_terminator_only() -> None: + """WorkflowRunResult.get_outputs() returns only the terminator's yield.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + + workflow = SequentialBuilder(participants=[a, b]).build() + result = await workflow.run("hi") + outputs = result.get_outputs() + assert len(outputs) == 1 + + +# --------------------------------------------------------------------------- +# Concurrent +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_concurrent_default_only_aggregator_is_output() -> None: + """Default Concurrent designates only the aggregator; participants are hidden.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + + workflow = ConcurrentBuilder(participants=[a, b]).build() + + output_events: list[Any] = [] + intermediate_events: list[Any] = [] + async for event in workflow.run("hello", stream=True): + if event.type == "output": + output_events.append(event) + elif event.type == "intermediate": + intermediate_events.append(event) + + # Aggregator is the only designated executor → only it emits type='output'. + assert len(output_events) == 1 + + assert not intermediate_events + + +@pytest.mark.asyncio +async def test_concurrent_output_from_designates_workflow_output_participants() -> None: + """Concurrent output_from designates participant outputs alongside the aggregator.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + + workflow = ConcurrentBuilder(participants=[a, b], output_from=[a, "B"]).build() + result = await workflow.run("hello") + outputs = result.get_outputs() + assert len(outputs) == 3 + + +@pytest.mark.asyncio +async def test_concurrent_intermediate_output_from_surface_as_intermediate() -> None: + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + + workflow = ConcurrentBuilder(participants=[a, b], intermediate_output_from=["A", b]).build() + + output_executors: set[str] = set() + intermediate_executors: set[str] = set() + async for event in workflow.run("hello", stream=True): + if event.type == "output" and event.executor_id is not None: + output_executors.add(event.executor_id) + elif event.type == "intermediate" and event.executor_id is not None: + intermediate_executors.add(event.executor_id) + + assert "aggregator" in output_executors + assert intermediate_executors == {"A", "B"} + + +# --------------------------------------------------------------------------- +# Sequential wrapped as_agent +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_sequential_default_as_agent_forwards_original_content_types() -> None: + """Default Sequential wrapped as_agent forwards original content types.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c]).build() + agent = workflow.as_agent("seq") + + response = await agent.run("hi") + + text_contents = [c for m in response.messages for c in m.contents if c.type == "text"] + reasoning_contents = [c for m in response.messages for c in m.contents if c.type == "text_reasoning"] + + assert any("C reply" in c.text for c in text_contents) + assert not reasoning_contents + + +@pytest.mark.asyncio +async def test_sequential_as_agent_output_from_all_text() -> None: + """output_from makes designated participant replies normal response text content.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c], output_from=["A", "B", "C"]).build() + agent = workflow.as_agent("seq") + + response = await agent.run("hi") + text_contents = [c for m in response.messages for c in m.contents if c.type == "text"] + text = " ".join(c.text for c in text_contents) + assert "A reply" in text + assert "B reply" in text + assert "C reply" in text + + +@pytest.mark.asyncio +async def test_sequential_as_agent_intermediate_output_from_keeps_text_content() -> None: + """intermediate_output_from keeps selected participant replies as their original content type.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c], intermediate_output_from=["A", "B"]).build() + agent = workflow.as_agent("seq") + + response = await agent.run("hi") + + text_contents = [c for m in response.messages for c in m.contents if c.type == "text"] + reasoning_contents = [c for m in response.messages for c in m.contents if c.type == "text_reasoning"] + assert any("C reply" in c.text for c in text_contents) + assert any("A reply" in c.text for c in text_contents) + assert any("B reply" in c.text for c in text_contents) + assert not reasoning_contents + + +# --------------------------------------------------------------------------- +# Concurrent wrapped as_agent +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_concurrent_default_as_agent_participants_keep_text_content() -> None: + """Default Concurrent wrapped as_agent keeps original participant content types.""" + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + + workflow = ConcurrentBuilder(participants=[a, b]).build() + agent = workflow.as_agent("concurrent") + + response = await agent.run("hi") + + text_contents = [c for m in response.messages for c in m.contents if c.type == "text"] + reasoning_contents = [c for m in response.messages for c in m.contents if c.type == "text_reasoning"] + + assert not any("A reply" in c.text for c in reasoning_contents) + assert not any("B reply" in c.text for c in reasoning_contents) + + # The aggregator's default-yielded AgentResponse passes through as text content. + assert text_contents, "expected at least one terminal text content from the aggregator" + + +# --------------------------------------------------------------------------- +# GroupChat +# --------------------------------------------------------------------------- + + +def _two_step_selector() -> Callable[[GroupChatState], str]: + """Selector that picks each participant once, then keeps the first to keep tests bounded.""" + counter = {"n": 0} + + def _select(state: GroupChatState) -> str: + participants = list(state.participants.keys()) + step = counter["n"] + counter["n"] = step + 1 + if step == 0: + return participants[0] + if step == 1 and len(participants) > 1: + return participants[1] + return participants[0] + + return _select + + +@pytest.mark.asyncio +async def test_group_chat_default_only_orchestrator_is_output() -> None: + """Default GroupChat designates only the orchestrator; participant replies are hidden.""" + alpha = _EchoAgent(name="alpha") + beta = _EchoAgent(name="beta") + + workflow = GroupChatBuilder( + participants=[alpha, beta], + max_rounds=2, + selection_func=_two_step_selector(), + ).build() + + output_executors: set[str] = set() + intermediate_executors: set[str] = set() + async for event in workflow.run("kickoff", stream=True): + if event.type == "output" and event.executor_id is not None: + output_executors.add(event.executor_id) + elif event.type == "intermediate" and event.executor_id is not None: + intermediate_executors.add(event.executor_id) + + assert "group_chat_orchestrator" in output_executors + assert "alpha" not in intermediate_executors + assert "beta" not in intermediate_executors + # Participants must NOT appear among designated outputs in the default contract. + assert "alpha" not in output_executors + assert "beta" not in output_executors + + +@pytest.mark.asyncio +async def test_group_chat_output_from_designates_workflow_output_participants() -> None: + """GroupChat output_from designates participants alongside the orchestrator.""" + alpha = _EchoAgent(name="alpha") + beta = _EchoAgent(name="beta") + + workflow = GroupChatBuilder( + participants=[alpha, beta], + max_rounds=2, + selection_func=_two_step_selector(), + output_from=[alpha, "beta"], + ).build() + + output_executors: set[str] = set() + async for event in workflow.run("kickoff", stream=True): + if event.type == "output" and event.executor_id is not None: + output_executors.add(event.executor_id) + + assert {"group_chat_orchestrator", "alpha", "beta"}.issubset(output_executors) + + +@pytest.mark.asyncio +async def test_group_chat_intermediate_output_from_surface_as_intermediate() -> None: + alpha = _EchoAgent(name="alpha") + beta = _EchoAgent(name="beta") + + workflow = GroupChatBuilder( + participants=[alpha, beta], + max_rounds=2, + selection_func=_two_step_selector(), + intermediate_output_from=["alpha", beta], + ).build() + + output_executors: set[str] = set() + intermediate_executors: set[str] = set() + async for event in workflow.run("kickoff", stream=True): + if event.type == "output" and event.executor_id is not None: + output_executors.add(event.executor_id) + elif event.type == "intermediate" and event.executor_id is not None: + intermediate_executors.add(event.executor_id) + + assert "group_chat_orchestrator" in output_executors + assert intermediate_executors == {"alpha", "beta"} + + +# --------------------------------------------------------------------------- +# Handoff +# --------------------------------------------------------------------------- + + +def test_handoff_builder_designates_every_participant_as_output() -> None: + """Handoff has no intermediate channel — every participant's reply is a primary + output. The builder must designate all participants in the workflow's + output designation so each per-agent yield surfaces as type='output'. + + Structural assertion (vs end-to-end) because Handoff agents require a full + chat-client/middleware stack that we don't want to reproduce in this contract test. + """ + from agent_framework import Agent + from agent_framework._clients import BaseChatClient + from agent_framework._middleware import ChatMiddlewareLayer + from agent_framework._tools import FunctionInvocationLayer + + class _StubClient(FunctionInvocationLayer[Any], ChatMiddlewareLayer[Any], BaseChatClient[Any]): + def __init__(self) -> None: + ChatMiddlewareLayer.__init__(self) + FunctionInvocationLayer.__init__(self) + BaseChatClient.__init__(self) + + def _inner_get_response(self, **kwargs: Any) -> Any: # pragma: no cover - never called + raise NotImplementedError + + alpha = Agent( + name="alpha", + id="alpha", + client=_StubClient(), + require_per_service_call_history_persistence=True, + ) + beta = Agent( + name="beta", + id="beta", + client=_StubClient(), + require_per_service_call_history_persistence=True, + ) + + workflow = HandoffBuilder(participants=[alpha, beta]).with_start_agent(alpha).build() + + designated = {ex.id for ex in workflow.get_output_executors()} + assert "alpha" in designated, f"alpha must be designated; got {designated}" + assert "beta" in designated, f"beta must be designated; got {designated}" + + +def test_handoff_builder_output_from_can_select_workflow_output_participants() -> None: + from agent_framework import Agent + from agent_framework._clients import BaseChatClient + from agent_framework._middleware import ChatMiddlewareLayer + from agent_framework._tools import FunctionInvocationLayer + + class _StubClient(FunctionInvocationLayer[Any], ChatMiddlewareLayer[Any], BaseChatClient[Any]): + def __init__(self) -> None: + ChatMiddlewareLayer.__init__(self) + FunctionInvocationLayer.__init__(self) + BaseChatClient.__init__(self) + + def _inner_get_response(self, **kwargs: Any) -> Any: # pragma: no cover - never called + raise NotImplementedError + + alpha = Agent( + name="alpha", + id="alpha", + client=_StubClient(), + require_per_service_call_history_persistence=True, + ) + beta = Agent( + name="beta", + id="beta", + client=_StubClient(), + require_per_service_call_history_persistence=True, + ) + + workflow = HandoffBuilder(participants=[alpha, beta], output_from=["alpha"]).with_start_agent(alpha).build() + + assert {ex.id for ex in workflow.get_output_executors()} == {"alpha"} + + +def test_handoff_builder_intermediate_output_from_demotes_from_default_output() -> None: + """Regression: `intermediate_output_from` alone must not collide with the default output list. + + Handoff defaults workflow output to every participant. Before the fix, supplying + `intermediate_output_from=["alpha"]` without restating `output_from` triggered + "Participants cannot be both output and intermediate designated: ['alpha']" because + alpha was simultaneously in the default output list and the explicit intermediate list. + The contract documented at `_handoff.py:619-622` promises `intermediate_output_from` is + usable on its own. + """ + from agent_framework import Agent + from agent_framework._clients import BaseChatClient + from agent_framework._middleware import ChatMiddlewareLayer + from agent_framework._tools import FunctionInvocationLayer + + class _StubClient(FunctionInvocationLayer[Any], ChatMiddlewareLayer[Any], BaseChatClient[Any]): + def __init__(self) -> None: + ChatMiddlewareLayer.__init__(self) + FunctionInvocationLayer.__init__(self) + BaseChatClient.__init__(self) + + def _inner_get_response(self, **kwargs: Any) -> Any: # pragma: no cover - never called + raise NotImplementedError + + alpha = Agent(name="alpha", id="alpha", client=_StubClient(), require_per_service_call_history_persistence=True) + beta = Agent(name="beta", id="beta", client=_StubClient(), require_per_service_call_history_persistence=True) + + workflow = ( + HandoffBuilder(participants=[alpha, beta], intermediate_output_from=["alpha"]).with_start_agent(alpha).build() + ) + + # alpha is implicitly removed from the default-final set; beta remains final. + assert {ex.id for ex in workflow.get_output_executors()} == {"beta"} + assert {ex.id for ex in workflow.get_intermediate_executors()} == {"alpha"} + + +# --------------------------------------------------------------------------- +# Magentic +# --------------------------------------------------------------------------- + + +class _StubMagenticManager(MagenticManagerBase): + """Deterministic manager that finishes after one round with a fixed final answer.""" + + FINAL_ANSWER: ClassVar[str] = "MAGENTIC_FINAL" + + def __init__(self) -> None: + super().__init__(max_stall_count=3) + self.name = "magentic_manager" + self.next_speaker_name = "alpha" + + async def plan(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["Plan: do the thing."], author_name=self.name) + + async def replan(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", ["Replan."], author_name=self.name) + + async def create_progress_ledger(self, magentic_context: MagenticContext) -> MagenticProgressLedger: + is_satisfied = len(magentic_context.chat_history) > 1 + return MagenticProgressLedger( + is_request_satisfied=MagenticProgressLedgerItem(reason="t", answer=is_satisfied), + is_in_loop=MagenticProgressLedgerItem(reason="t", answer=False), + is_progress_being_made=MagenticProgressLedgerItem(reason="t", answer=True), + next_speaker=MagenticProgressLedgerItem(reason="t", answer=self.next_speaker_name), + instruction_or_question=MagenticProgressLedgerItem(reason="t", answer="Go."), + ) + + async def prepare_final_answer(self, magentic_context: MagenticContext) -> Message: + return Message("assistant", [self.FINAL_ANSWER], author_name=self.name) + + +def test_magentic_builder_default_only_manager_designated() -> None: + """Default Magentic: only the orchestrator (manager) is designated for terminal output; + participant replies surface as type='intermediate'. + + Structural assertion on the workflow's output designation because exercising a Magentic + plan/replan loop end-to-end is heavy and orthogonal to this contract. + """ + manager = _StubMagenticManager() + alpha = _EchoAgent(name="alpha") + + workflow = MagenticBuilder(participants=[alpha], manager=manager).build() + + designated = {ex.id for ex in workflow.get_output_executors()} + assert "magentic_orchestrator" in designated, f"manager must be designated; got {designated}" + assert "alpha" not in designated, f"participant must not be designated by default; got {designated}" + + +def test_magentic_builder_output_from_designates_workflow_output_participants() -> None: + """Magentic output_from designates workers alongside the orchestrator.""" + manager = _StubMagenticManager() + alpha = _EchoAgent(name="alpha") + + workflow = MagenticBuilder(participants=[alpha], manager=manager, output_from=["alpha"]).build() + + designated = {ex.id for ex in workflow.get_output_executors()} + assert {"magentic_orchestrator", "alpha"}.issubset(designated) + + +def test_magentic_builder_intermediate_output_from_designates_intermediate_workers() -> None: + manager = _StubMagenticManager() + alpha = _EchoAgent(name="alpha") + + workflow = MagenticBuilder(participants=[alpha], manager=manager, intermediate_output_from=[alpha]).build() + + assert {ex.id for ex in workflow.get_output_executors()} == {"magentic_orchestrator"} + assert {ex.id for ex in workflow.get_intermediate_executors()} == {"alpha"} + + +def test_sequential_output_from_all_selects_all_participants() -> None: + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder(participants=[a, b, c], output_from="all").build() + + assert {ex.id for ex in workflow.get_output_executors()} == {"A", "B", "C"} + + +def test_sequential_intermediate_output_from_all_other_selects_non_outputs() -> None: + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + c = _EchoAgent(name="C") + + workflow = SequentialBuilder( + participants=[a, b, c], output_from=["C"], intermediate_output_from="all_other" + ).build() + + assert {ex.id for ex in workflow.get_output_executors()} == {"C"} + assert {ex.id for ex in workflow.get_intermediate_executors()} == {"A", "B"} + + +def test_sequential_all_other_with_omitted_output_from_selects_all_intermediate() -> None: + a = _EchoAgent(name="A") + b = _EchoAgent(name="B") + + workflow = SequentialBuilder(participants=[a, b], intermediate_output_from="all_other").build() + + assert {ex.id for ex in workflow.get_output_executors()} == set() + assert {ex.id for ex in workflow.get_intermediate_executors()} == {"A", "B"} + + +# --------------------------------------------------------------------------- +# Participant designation validation +# --------------------------------------------------------------------------- + + +def _build_sequential_with_designation(**kwargs: Any) -> None: + SequentialBuilder(participants=[_EchoAgent(name="alpha"), _EchoAgent(name="beta")], **kwargs).build() + + +def _build_concurrent_with_designation(**kwargs: Any) -> None: + ConcurrentBuilder(participants=[_EchoAgent(name="alpha"), _EchoAgent(name="beta")], **kwargs).build() + + +def _build_group_chat_with_designation(**kwargs: Any) -> None: + GroupChatBuilder( + participants=[_EchoAgent(name="alpha"), _EchoAgent(name="beta")], + max_rounds=1, + selection_func=_two_step_selector(), + **kwargs, + ).build() + + +def _build_magentic_with_designation(**kwargs: Any) -> None: + MagenticBuilder(participants=[_EchoAgent(name="alpha")], manager=_StubMagenticManager(), **kwargs).build() + + +def _build_handoff_with_designation(**kwargs: Any) -> None: + from agent_framework import Agent + from agent_framework._clients import BaseChatClient + from agent_framework._middleware import ChatMiddlewareLayer + from agent_framework._tools import FunctionInvocationLayer + + class _StubClient(FunctionInvocationLayer[Any], ChatMiddlewareLayer[Any], BaseChatClient[Any]): + def __init__(self) -> None: + ChatMiddlewareLayer.__init__(self) + FunctionInvocationLayer.__init__(self) + BaseChatClient.__init__(self) + + def _inner_get_response(self, **kwargs: Any) -> Any: # pragma: no cover - never called + raise NotImplementedError + + alpha = Agent( + name="alpha", + id="alpha", + client=_StubClient(), + require_per_service_call_history_persistence=True, + ) + beta = Agent( + name="beta", + id="beta", + client=_StubClient(), + require_per_service_call_history_persistence=True, + ) + HandoffBuilder(participants=[alpha, beta], **kwargs).with_start_agent(alpha).build() + + +@pytest.mark.parametrize( + "build", + [ + _build_sequential_with_designation, + _build_concurrent_with_designation, + _build_group_chat_with_designation, + _build_magentic_with_designation, + _build_handoff_with_designation, + ], +) +@pytest.mark.parametrize( + ("kwargs", "match"), + [ + ({"output_from": [], "intermediate_output_from": []}, "cannot both be empty"), + ({"output_from": ["alpha", "alpha"]}, "Duplicate output participant"), + ({"output_from": ["alpha"], "intermediate_output_from": ["alpha"]}, "cannot be both output"), + ({"output_from": ["missing"]}, "Unknown output participant"), + ({"output_from": "all_other"}, "output_from='all_other'"), + ], +) +def test_participant_output_config_validation(build: Callable[..., None], kwargs: dict[str, Any], match: str) -> None: + with pytest.raises(ValueError, match=match): + build(**kwargs) + + +@pytest.mark.parametrize( + "build", + [ + _build_sequential_with_designation, + _build_concurrent_with_designation, + _build_group_chat_with_designation, + _build_magentic_with_designation, + _build_handoff_with_designation, + ], +) +def test_participant_output_config_rejects_final_output_from_parameter(build: Callable[..., None]) -> None: + with pytest.raises(TypeError, match="final_output_from"): + build(final_output_from=["beta"]) diff --git a/python/samples/03-workflows/README.md b/python/samples/03-workflows/README.md index d4e6fc1fb1..c79203d742 100644 --- a/python/samples/03-workflows/README.md +++ b/python/samples/03-workflows/README.md @@ -89,6 +89,7 @@ Write workflows as plain Python async functions — no graph concepts, no execut | Multi-Selection Edge Group | [control-flow/multi_selection_edge_group.py](./control-flow/multi_selection_edge_group.py) | Select one or many targets dynamically (subset fan-out) | | Simple Loop | [control-flow/simple_loop.py](./control-flow/simple_loop.py) | Feedback loop where an agent judges ABOVE/BELOW/MATCHED | | Workflow Cancellation | [control-flow/workflow_cancellation.py](./control-flow/workflow_cancellation.py) | Cancel a running workflow using asyncio tasks | +| Workflow and Intermediate Outputs | [control-flow/intermediate_vs_terminal_outputs.py](./control-flow/intermediate_vs_terminal_outputs.py) | Select Workflow Output and Intermediate Output executors; hide unselected yields; map Intermediate Output events to `text_reasoning` content via `as_agent` | ### human-in-the-loop @@ -118,6 +119,43 @@ For additional observability samples in Agent Framework, see the [observability Orchestration-focused samples (Sequential, Concurrent, Handoff, GroupChat, Magentic), including builder-based `workflow.as_agent(...)` variants, are documented in the [orchestrations](./orchestrations/README.md) directory. +### output selection + +Workflow Output selection controls which `ctx.yield_output(...)` calls are visible to callers as `type='output'` +events and through `WorkflowRunResult.get_outputs()`. The core rule is that `output_from` is an allow-list for +Workflow Output, not a routing rule for every other executor output. Unselected executor payloads are hidden unless +`intermediate_output_from` explicitly selects them as Intermediate Output. + +Use `output_from` and `intermediate_output_from` as the canonical API: + +| Selection | Workflow Output | Intermediate Output | Hidden payloads | +| --- | --- | --- | --- | +| Omit both selections | Every executor `yield_output`; emits a deprecation warning | None | None | +| `output_from="all"` | Every executor `yield_output`; no warning | None | None | +| `output_from=[answerer]` | Only `answerer` | None | All other executor payloads | +| `output_from=[answerer], intermediate_output_from="all_other"` | Only `answerer` | Every output-capable executor not selected by `output_from` | None | +| `intermediate_output_from="all_other"` | None | Every output-capable executor | None | +| `output_from=[], intermediate_output_from="all_other"` | None | Every output-capable executor | None | +| `output_from=[answerer], intermediate_output_from=[planner, researcher]` | Only `answerer` | `planner` and `researcher` | Any other executor payloads | + +Invalid selections fail at construction or build time: + +| Invalid selection | Why it fails | +| --- | --- | +| `output_from="all_other"` | `"all_other"` is only valid for `intermediate_output_from` | +| `intermediate_output_from="all"` | `"all"` is only valid for `output_from` | +| The same executor in both selections | One payload cannot be both Workflow Output and Intermediate Output | +| Duplicate executor selections | Duplicates are treated as configuration errors | +| Unknown executor selections | Typos and missing participants are rejected | +| `output_from=[], intermediate_output_from=[]` | Both explicit selections are empty | + +Compatibility aliases such as `output_executors` emit deprecation warnings where supported. New samples and +applications should use `output_from` and `intermediate_output_from`. + +When a workflow is wrapped with `workflow.as_agent()`, Workflow Output becomes normal agent text content. Intermediate +Output becomes `text_reasoning` content, so `AgentResponse.text` remains focused on the caller-facing answer while +callers can still inspect progress or supporting work from the response messages. + ### parallelism | Sample | File | Concepts | @@ -174,7 +212,7 @@ Sequential orchestration uses a few small adapter nodes for plumbing: - "input-conversation" normalizes input to `list[Message]` - "to-conversation:" converts agent responses into the shared conversation -- "complete" publishes the final output event (type='output') +- "complete" publishes the Workflow Output event (`type='output'`) These may appear in event streams (executor_invoked/executor_completed). They're analogous to concurrent’s dispatcher and aggregator and can be ignored if you only care about agent activity. diff --git a/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py b/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py index b503f7574f..b156858484 100644 --- a/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/group_chat_workflow_as_agent.py @@ -54,11 +54,11 @@ async def main() -> None: credential=AzureCliCredential(), ) - # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds - # (Intermediate outputs will be emitted as WorkflowOutputEvent events) + # Mark participant responses as intermediate so workflow.as_agent() maps + # them to text_reasoning content while the final answer remains normal text. workflow = GroupChatBuilder( participants=[researcher, writer], - intermediate_outputs=True, + intermediate_output_from=[researcher, writer], orchestrator_agent=Agent( client=_orch_client, name="Orchestrator", diff --git a/python/samples/03-workflows/agents/magentic_workflow_as_agent.py b/python/samples/03-workflows/agents/magentic_workflow_as_agent.py index 6cc91a9dcd..488cd91f20 100644 --- a/python/samples/03-workflows/agents/magentic_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/magentic_workflow_as_agent.py @@ -72,11 +72,11 @@ async def main() -> None: print("\nBuilding Magentic Workflow...") - # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds - # (Intermediate outputs will be emitted as WorkflowOutputEvent events) + # Mark participant responses as intermediate so workflow.as_agent() maps + # them to text_reasoning content while the final answer remains normal text. workflow = MagenticBuilder( participants=[researcher_agent, coder_agent], - intermediate_outputs=True, + intermediate_output_from=[researcher_agent, coder_agent], manager_agent=manager_agent, max_round_count=10, max_stall_count=3, diff --git a/python/samples/03-workflows/agents/sequential_workflow_as_agent.py b/python/samples/03-workflows/agents/sequential_workflow_as_agent.py index 120bd448aa..69dfecd410 100644 --- a/python/samples/03-workflows/agents/sequential_workflow_as_agent.py +++ b/python/samples/03-workflows/agents/sequential_workflow_as_agent.py @@ -77,9 +77,9 @@ async def main() -> None: Note: `workflow.as_agent()` returns ONLY the final agent's response (the "answer") — the prior agents' work - is not included in the response. To observe intermediate agents while running as an agent, build with - `SequentialBuilder(participants=[...], intermediate_outputs=True)`; the intermediate replies are then - surfaced as `data` events and merged into the AgentResponse. + is not included in the response. To preserve earlier participant replies while running as an agent, build with + `SequentialBuilder(participants=[...], intermediate_output_from=[writer])`; intermediate workflow events become + `text_reasoning` content on the AgentResponse, while `.text` remains terminal-output only. """ diff --git a/python/samples/03-workflows/control-flow/intermediate_vs_terminal_outputs.py b/python/samples/03-workflows/control-flow/intermediate_vs_terminal_outputs.py new file mode 100644 index 0000000000..520c17a07b --- /dev/null +++ b/python/samples/03-workflows/control-flow/intermediate_vs_terminal_outputs.py @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import ( + Message, + WorkflowBuilder, + WorkflowContext, + WorkflowExecutor, + executor, +) +from typing_extensions import Never + +""" +Sample: Workflow Output vs Intermediate Output labeling + +What this sample shows +- How ``WorkflowBuilder(output_from=[...])`` designates which executors emit + Workflow Output. +- How ``WorkflowBuilder(intermediate_output_from=[...])`` designates which executor + yields surface as Intermediate Output (``type='intermediate'`` events). +- How unlisted executor yields are hidden from caller-facing output/intermediate + events in explicit designation mode. +- How the same workflow wrapped via ``workflow.as_agent()`` translates intermediate + events to ``text_reasoning`` content so existing ``.text`` accessors keep + returning Workflow Output only. +- How a sub-workflow embedded via ``WorkflowExecutor`` bubbles its intermediate + emissions up through the parent's event stream, attributed to the + ``WorkflowExecutor`` id rather than the child's internal executor ids. + +The output selection contract: +- Compatibility mode: when neither ``output_from`` nor ``intermediate_output_from`` + is provided, every ``yield_output`` produces Workflow Output and a deprecation + warning points to explicit selection. +- Explicit selection mode: provide either ``output_from`` or + ``intermediate_output_from``. Executors selected by ``output_from`` emit Workflow Output + (``type='output'`` events); executors selected by ``intermediate_output_from`` emit + Intermediate Output (``type='intermediate'`` events); unselected executor yields are + hidden from the stream and ``WorkflowRunResult`` output accessors. +- Validation: explicit selections must not both be empty; duplicate executor entries, + overlap between Workflow Output and Intermediate Output, unknown executors, invalid + literals, and selected executors without workflow output types are rejected. + +Prerequisites +- No external services required. +""" + + +@executor(id="planner") +async def planner(messages: list[Message], ctx: WorkflowContext[list[Message], str]) -> None: + """Intermediate step: emits a visible progress note, then forwards.""" + prompt = messages[0].text if messages else "" + await ctx.yield_output(f"plan: starting work on '{prompt}'") + await ctx.send_message(messages) + + +@executor(id="researcher") +async def researcher(messages: list[Message], ctx: WorkflowContext[list[Message], str]) -> None: + """Intermediate step: emits visible progress, then forwards.""" + prompt = messages[0].text if messages else "" + await ctx.yield_output(f"research: gathering data for '{prompt}'") + await ctx.send_message(messages) + + +@executor(id="answerer") +async def answerer(messages: list[Message], ctx: WorkflowContext[Never, str]) -> None: + """Designated Workflow Output: emits the workflow's answer.""" + prompt = messages[0].text if messages else "" + await ctx.yield_output(f"final answer to '{prompt}': 42") + + +async def main() -> None: + # Build with explicit Workflow Output and Intermediate Output selections. + # `answerer` produces type='output' events; planner and researcher produce + # visible type='intermediate' events. + workflow = ( + WorkflowBuilder( + start_executor=planner, + output_from=[answerer], + intermediate_output_from=[planner, researcher], + ) + .add_edge(planner, researcher) + .add_edge(researcher, answerer) + .build() + ) + + initial = [Message(role="user", contents=["life, the universe, and everything"])] + + print("=== Streaming events (workflow.run(stream=True)) ===") + async for event in workflow.run(initial, stream=True): + if event.type == "intermediate": + print(f" [intermediate] {event.executor_id}: {event.data}") + elif event.type == "output": + print(f" [output] {event.executor_id}: {event.data}") + + # WorkflowRunResult.get_outputs() filters to type='output' events, so it + # only returns the selected Workflow Output yield. + print("\n=== Non-streaming run().get_outputs() ===") + result = await workflow.run(initial) + print(f" outputs: {result.get_outputs()}") + + # When the same workflow is wrapped via as_agent(), intermediate events + # surface as ``text_reasoning`` content; Workflow Output surfaces as + # ``text`` content. Existing callers reading ``response.text`` get only + # the selected Workflow Output because ``.text`` filters to text content. + print("\n=== workflow.as_agent() -- intermediate -> text_reasoning content ===") + agent = workflow.as_agent("planner-agent") + response = await agent.run("life, the universe, and everything") + print(f" response.text (Workflow Output only): {response.text!r}") + reasoning = " | ".join(c.text for m in response.messages for c in m.contents if c.type == "text_reasoning") + print(f" reasoning content (intermediates): {reasoning!r}") + + # Embed the same workflow as a node inside a larger workflow via WorkflowExecutor. + # Child intermediate emissions are forwarded to the parent's event stream with the + # WorkflowExecutor's id as the source, so outer callers don't have to know the + # child's internal executor layout. The 'intermediate' label is preserved across + # the boundary regardless of how the parent designates the WorkflowExecutor. + print("\n=== Embedding as a sub-workflow -- intermediates bubble up ===") + sub = WorkflowExecutor(workflow, id="sub") + + @executor(id="parent_sink") + async def parent_sink(message: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(message) + + parent_workflow = WorkflowBuilder(start_executor=sub, output_from=[parent_sink]).add_edge(sub, parent_sink).build() + + async for event in parent_workflow.run(initial, stream=True): + if event.type == "intermediate": + print(f" [intermediate] {event.executor_id}: {event.data}") + elif event.type == "output": + print(f" [output] {event.executor_id}: {event.data}") + + """ + Sample output: + + === Streaming events (workflow.run(stream=True)) === + [intermediate] planner: plan: starting work on 'life, the universe, and everything' + [intermediate] researcher: research: gathering data for 'life, the universe, and everything' + [output] answerer: final answer to 'life, the universe, and everything': 42 + + === Non-streaming run().get_outputs() === + outputs: ["final answer to 'life, the universe, and everything': 42"] + + === workflow.as_agent() -- intermediate -> text_reasoning content === + response.text (Workflow Output only): "final answer to 'life, the universe, and everything': 42" + reasoning content (intermediates): "plan: starting work on ... | research: gathering data for ..." + + === Embedding as a sub-workflow -- intermediates bubble up === + [intermediate] sub: plan: starting work on 'life, the universe, and everything' + [intermediate] sub: research: gathering data for 'life, the universe, and everything' + [output] parent_sink: final answer to 'life, the universe, and everything': 42 + """ + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py b/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py index adb1fff4d5..6889914dfd 100644 --- a/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py +++ b/python/samples/03-workflows/human-in-the-loop/agents_with_approval_requests.py @@ -248,7 +248,7 @@ async def main() -> None: # Build the workflow workflow = ( - WorkflowBuilder(start_executor=email_processor, output_executors=[conclude_workflow]) + WorkflowBuilder(start_executor=email_processor, output_from=[conclude_workflow]) .add_edge(email_processor, email_writer_agent) .add_edge(email_writer_agent, conclude_workflow) .build() diff --git a/python/samples/03-workflows/orchestrations/README.md b/python/samples/03-workflows/orchestrations/README.md index 0c4406c247..c0cd65b650 100644 --- a/python/samples/03-workflows/orchestrations/README.md +++ b/python/samples/03-workflows/orchestrations/README.md @@ -81,6 +81,41 @@ from agent_framework.orchestrations import ( ## Tips +**Participant output selection**: Orchestration builders use participant-oriented names for Workflow Output selection. +Use `output_from=[...]` when participant responses should be Workflow Output (`type='output'` events), and +`intermediate_output_from=[...]` when participant responses should be Intermediate Output (`type='intermediate'` +events). `output_from` is an allow-list for Workflow Output, not a routing rule for every other participant output. +Unselected participant responses are hidden unless `intermediate_output_from` selects them. + +| Selection | Workflow Output | Intermediate Output | Hidden payloads | +| --- | --- | --- | --- | +| Omit both selections | Builder default Workflow Output contract | None | Builder-specific non-output participant payloads | +| `output_from="all"` | Every output-capable participant | None | None | +| `output_from=[writer]` | Only `writer` | None | All other participant payloads | +| `output_from=[writer], intermediate_output_from="all_other"` | Only `writer` | Every output-capable participant not selected by `output_from` | None | +| `intermediate_output_from="all_other"` | None, except builder-internal default output executors where applicable | Every output-capable participant | Builder-internal plumbing payloads | +| `output_from=[], intermediate_output_from="all_other"` | None, except builder-internal default output executors where applicable | Every output-capable participant | Builder-internal plumbing payloads | +| `output_from=[writer], intermediate_output_from=[researcher, reviewer]` | Only `writer` | `researcher` and `reviewer` | Any other participant payloads | + +Invalid selections fail at construction or build time: + +| Invalid selection | Why it fails | +| --- | --- | +| `output_from="all_other"` | `"all_other"` is only valid for `intermediate_output_from` | +| `intermediate_output_from="all"` | `"all"` is only valid for `output_from` | +| The same participant in both selections | One payload cannot be both Workflow Output and Intermediate Output | +| Duplicate participant selections | Duplicates are treated as configuration errors | +| Unknown participant selections | Typos and missing participants are rejected | +| `output_from=[], intermediate_output_from=[]` | Both explicit selections are empty | + +By default, Sequential keeps the last participant as Workflow Output. Concurrent, GroupChat, and Magentic keep their +synthetic aggregator/orchestrator/manager executors as Workflow Output, while participant responses stay hidden unless +selected. Handoff keeps participants as Workflow Output by default. + +When an orchestration workflow is exposed via `workflow.as_agent()`, Workflow Output becomes normal text content in +the `AgentResponse`; Intermediate Output becomes `text_reasoning` content. This preserves `.text` while making +selected progress available for callers that inspect message contents. + **Magentic checkpointing tip**: Treat `MagenticBuilder.participants` keys as stable identifiers. When resuming from a checkpoint, the rebuilt workflow must reuse the same participant names; otherwise the checkpoint cannot be applied and the run will fail fast. **Handoff workflow tip**: Handoff workflows maintain the full conversation history including any `Message.additional_properties` emitted by your agents. This ensures routing metadata remains intact across all agent transitions. For specialist-to-specialist handoffs, use `.add_handoff(source, targets)` to configure which agents can route to which others with a fluent, type-safe API. @@ -90,7 +125,7 @@ from agent_framework.orchestrations import ( **Sequential orchestration note**: Sequential orchestration uses a few small adapter nodes for plumbing: - `input-conversation` normalizes input to `list[Message]` - `to-conversation:` converts agent responses into the shared conversation -- `complete` publishes the final output event (type='output') +- `complete` publishes the Workflow Output event (`type='output'`) These may appear in event streams (executor_invoked/executor_completed). They're analogous to concurrent's dispatcher and aggregator and can be ignored if you only care about agent activity. diff --git a/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py b/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py index dea82a4352..079f66ba5c 100644 --- a/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py +++ b/python/samples/03-workflows/orchestrations/group_chat_agent_manager.py @@ -78,13 +78,14 @@ async def main() -> None: # Build the group chat workflow # termination_condition: stop after 4 assistant messages # (The agent orchestrator will intelligently decide when to end before this limit but just in case) - # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds - # (Intermediate outputs will be emitted as WorkflowOutputEvent events) + # Mark participant responses as intermediate so the stream shows the + # conversation as it unfolds while the orchestrator's transcript remains the + # terminal workflow output. workflow = ( GroupChatBuilder( participants=[researcher, writer], termination_condition=lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4, - intermediate_outputs=True, + intermediate_output_from=[researcher, writer], orchestrator_agent=orchestrator_agent, ) # Set a hard termination condition: stop after 4 assistant messages @@ -102,7 +103,7 @@ async def main() -> None: # Keep track of the last response to format output nicely in streaming mode last_response_id: str | None = None async for event in workflow.run(task, stream=True): - if event.type == "output": + if event.type in ("intermediate", "output"): data = event.data if isinstance(data, AgentResponseUpdate): rid = data.response_id diff --git a/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py b/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py index 867bbd7bc3..259bd7bd5a 100644 --- a/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py +++ b/python/samples/03-workflows/orchestrations/group_chat_philosophical_debate.py @@ -219,13 +219,16 @@ Share your perspective authentically. Feel free to: ) # termination_condition: stop after 10 assistant messages - # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds - # (Intermediate outputs will be emitted as WorkflowOutputEvent events) + # Mark participant responses as intermediate so the stream shows the + # conversation as it unfolds while the orchestrator's transcript remains the + # terminal workflow output. workflow = ( GroupChatBuilder( participants=[farmer, developer, teacher, activist, spiritual_leader, artist, immigrant, doctor], termination_condition=lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10, - intermediate_outputs=True, + intermediate_output_from=[ + "all", + ], orchestrator_agent=moderator, ) .with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10) @@ -254,7 +257,7 @@ Share your perspective authentically. Feel free to: # Keep track of the last response to format output nicely in streaming mode last_response_id: str | None = None async for event in workflow.run(f"Please begin the discussion on: {topic}", stream=True): - if event.type == "output": + if event.type in ("intermediate", "output"): data = event.data if isinstance(data, AgentResponseUpdate): rid = data.response_id diff --git a/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py b/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py index 2fceaa98d0..fb20dcca59 100644 --- a/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py +++ b/python/samples/03-workflows/orchestrations/group_chat_simple_selector.py @@ -96,13 +96,14 @@ async def main() -> None: # This will end the conversation after the expert has spoken 2 times (one iteration loop) # Note: it's possible that the expert gets it right the first time and the other participants # have nothing to add, but for demo purposes we want to see at least one full round of interaction. - # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds - # (Intermediate outputs will be emitted as WorkflowOutputEvent events) + # Mark participant responses as intermediate so the stream shows the + # conversation as it unfolds while the orchestrator's transcript remains the + # terminal workflow output. workflow = ( GroupChatBuilder( participants=[expert, verifier, clarifier, skeptic], termination_condition=lambda conversation: len(conversation) >= 6, - intermediate_outputs=True, + intermediate_output_from=[expert, verifier, clarifier, skeptic], selection_func=round_robin_selector, ) # Set a hard termination condition: stop after 6 messages (user task + one full rounds + 1) @@ -123,7 +124,7 @@ async def main() -> None: # Keep track of the last response to format output nicely in streaming mode last_response_id: str | None = None async for event in workflow.run(task, stream=True): - if event.type == "output": + if event.type in ("intermediate", "output"): data = event.data if isinstance(data, AgentResponseUpdate): rid = data.response_id diff --git a/python/samples/03-workflows/orchestrations/magentic.py b/python/samples/03-workflows/orchestrations/magentic.py index f7a472049c..f750626955 100644 --- a/python/samples/03-workflows/orchestrations/magentic.py +++ b/python/samples/03-workflows/orchestrations/magentic.py @@ -88,11 +88,12 @@ async def main() -> None: print("\nBuilding Magentic Workflow...") - # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds - # (Intermediate outputs will be emitted as WorkflowOutputEvent events) + # Mark participant responses as intermediate so the stream shows the + # conversation as it unfolds while the manager's final answer remains the + # terminal workflow output. workflow = MagenticBuilder( participants=[researcher_agent, coder_agent], - intermediate_outputs=True, + intermediate_output_from=[researcher_agent, coder_agent], manager_agent=manager_agent, max_round_count=10, max_stall_count=3, @@ -115,7 +116,7 @@ async def main() -> None: last_response_id: str | None = None output_event: WorkflowEvent | None = None async for event in workflow.run(task, stream=True): - if event.type == "output" and isinstance(event.data, AgentResponseUpdate): + if event.type in ("intermediate", "output") and isinstance(event.data, AgentResponseUpdate): response_id = event.data.response_id if response_id != last_response_id: if last_response_id is not None: diff --git a/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py b/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py index e44e2a44ca..e6e254ed0d 100644 --- a/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py +++ b/python/samples/03-workflows/orchestrations/magentic_human_plan_review.py @@ -55,7 +55,7 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest: requests[event.request_id] = cast(MagenticPlanReviewRequest, event.data) - if event.type == "output": + if event.type in ("intermediate", "output"): data = event.data if isinstance(data, AgentResponseUpdate): rid = data.response_id @@ -129,13 +129,14 @@ async def main() -> None: print("\nBuilding Magentic Workflow with Human Plan Review...") - # enable_plan_review=True: Request human input for plan review - # intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds - # (Intermediate outputs will be emitted as WorkflowOutputEvent events) + # enable_plan_review=True: Request human input for plan review. + # Mark participant responses as intermediate so the stream shows the + # conversation as it unfolds while the manager's final answer remains the + # terminal workflow output. workflow = MagenticBuilder( participants=[researcher_agent, analyst_agent], enable_plan_review=True, - intermediate_outputs=True, + intermediate_output_from=[researcher_agent, analyst_agent], manager_agent=manager_agent, max_round_count=10, max_stall_count=1, diff --git a/python/samples/03-workflows/orchestrations/sequential_chain_only_agent_responses.py b/python/samples/03-workflows/orchestrations/sequential_chain_only_agent_responses.py index f4723a205d..d28fc49935 100644 --- a/python/samples/03-workflows/orchestrations/sequential_chain_only_agent_responses.py +++ b/python/samples/03-workflows/orchestrations/sequential_chain_only_agent_responses.py @@ -66,13 +66,13 @@ async def main() -> None: workflow = SequentialBuilder( participants=[writer, translator, reviewer], chain_only_agent_responses=True, - intermediate_outputs=True, + intermediate_output_from=[writer, translator], ).build() # 3) Run and collect outputs last_agent: str | None = None async for event in workflow.run("Write a tagline for a budget-friendly eBike.", stream=True): - if event.type == "output" and isinstance(event.data, AgentResponseUpdate): + if event.type in ("intermediate", "output") and isinstance(event.data, AgentResponseUpdate): if event.data.author_name != last_agent: last_agent = event.data.author_name print() diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/05_workflows/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/05_workflows/main.py index d70edbc7bf..5a2f2d6526 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/05_workflows/main.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/05_workflows/main.py @@ -52,9 +52,9 @@ def main(): workflow_agent = ( WorkflowBuilder( start_executor=writer_executor, - # Limiting the output to only the final formatted result. - # If this is not set, all intermediate results will be included in the output. - output_executors=[format_executor], + # Select only the formatted result as Workflow Output. + # Unselected executor payloads are hidden unless selected as Intermediate Output. + output_from=[format_executor], ) .add_edge(writer_executor, legal_executor) .add_edge(legal_executor, format_executor) diff --git a/python/samples/README.md b/python/samples/README.md index 0ff8563933..6017d578f6 100644 --- a/python/samples/README.md +++ b/python/samples/README.md @@ -8,7 +8,7 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen |--------|-------------| | [`01-get-started/`](./01-get-started/) | Progressive tutorial: hello agent → hosting | | [`02-agents/`](./02-agents/) | Deep-dive by concept: tools, middleware, providers, orchestrations | -| [`03-workflows/`](./03-workflows/) | Workflow patterns: sequential, concurrent, state, declarative | +| [`03-workflows/`](./03-workflows/) | Workflow patterns: sequential, concurrent, state, declarative, explicit output designation | | [`04-hosting/`](./04-hosting/) | Deployment: Azure Functions, Durable Tasks, A2A | | [`05-end-to-end/`](./05-end-to-end/) | Full applications, evaluation, demos | diff --git a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py index 51252a1786..89613072d8 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/group_chat.py +++ b/python/samples/semantic-kernel-migration/orchestrations/group_chat.py @@ -248,13 +248,13 @@ async def run_agent_framework_example(task: str) -> str: participants=[researcher, planner], orchestrator_agent=Agent(client=client), max_rounds=8, - intermediate_outputs=True, + intermediate_output_from=[researcher, planner], ).build() output_messages: list[Message] = [] last_message_id: str | None = None async for event in workflow.run(task, stream=True): - if event.type == "output": + if event.type in ("intermediate", "output"): if isinstance(event.data, AgentResponseUpdate): if event.data.message_id != last_message_id: last_message_id = event.data.message_id diff --git a/python/samples/semantic-kernel-migration/orchestrations/magentic.py b/python/samples/semantic-kernel-migration/orchestrations/magentic.py index dcf7b1af33..4ce62492e2 100644 --- a/python/samples/semantic-kernel-migration/orchestrations/magentic.py +++ b/python/samples/semantic-kernel-migration/orchestrations/magentic.py @@ -164,13 +164,13 @@ async def run_agent_framework_example(prompt: str) -> str | None: workflow = MagenticBuilder( participants=[researcher, coder], manager_agent=manager_agent, # type: ignore - intermediate_outputs=True, + intermediate_output_from=[researcher, coder], ).build() output_messages: list[Message] = [] last_message_id: str | None = None async for event in workflow.run(prompt, stream=True): - if event.type == "output": + if event.type in ("intermediate", "output"): if isinstance(event.data, AgentResponseUpdate): if event.data.message_id != last_message_id: last_message_id = event.data.message_id diff --git a/python/scripts/sample_validation/create_dynamic_workflow_executor.py b/python/scripts/sample_validation/create_dynamic_workflow_executor.py index f9356bbdd8..01af408097 100644 --- a/python/scripts/sample_validation/create_dynamic_workflow_executor.py +++ b/python/scripts/sample_validation/create_dynamic_workflow_executor.py @@ -17,8 +17,6 @@ from agent_framework.github import GitHubCopilotAgent from copilot.generated.session_events import PermissionRequest from copilot.session import PermissionRequestResult from pydantic import BaseModel -from typing_extensions import Never - from sample_validation.const import WORKER_COMPLETED from sample_validation.discovery import DiscoveryResult from sample_validation.models import ( @@ -29,6 +27,7 @@ from sample_validation.models import ( ValidationConfig, WorkflowCreationResult, ) +from typing_extensions import Never logger = logging.getLogger(__name__) @@ -249,7 +248,7 @@ class CollectorExecutor(Executor): batch_completion: BatchCompletion, ctx: WorkflowContext[Never, ExecutionResult], ) -> None: - """Receive all results at once and emit final output.""" + """Receive all results at once and emit Workflow Output.""" await ctx.yield_output(ExecutionResult(results=self._results)) @handler @@ -305,9 +304,7 @@ class CreateConcurrentValidationWorkflowExecutor(Executor): ) collector = CollectorExecutor() - nested_builder = WorkflowBuilder( - start_executor=coordinator, output_executors=[collector] - ) + nested_builder = WorkflowBuilder(start_executor=coordinator, output_from=[collector]) nested_builder.add_edge(coordinator, collector) for worker in workers: nested_builder.add_edge(coordinator, worker) From 1b6f7d80fde9d6db1c3dec47ca809ca0d698529b Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Mon, 18 May 2026 23:38:53 -0700 Subject: [PATCH 02/22] Python: Record actual served model from Azure OpenAI (#5910) * Record actual served model as response model for Azure OpenAI * Formatting * Fix tests * Fix pipeline error * Comments * Address review: surface served model via ChatResponse.model Apply blocking review feedback from PR #5910: - Use ChatResponse.model / ChatResponseUpdate.model as the source of truth for the Azure x-ms-served-model header value, instead of stashing it in additional_properties and overriding it again in observability. Observability already reads response.model; the chat client now overwrites it post-parse when the served-model header is present. Empirically the Azure Responses API returns the deployment alias in body.model and the actual snapshot (e.g. gpt-5-nano-2025-08-07) in this header. - Move the AZURE_OPENAI_SERVED_MODEL_HEADER constant out of observability.py and into RawOpenAIChatClient (as the SERVED_MODEL_HEADER ClassVar). The header is Azure-OpenAI-Responses-API-specific so observability does not need to know about it. - Revert the streaming text_format path to client.responses.stream(...) and drop the _pydantic_model_to_text_format_param helper. That helper imported from openai.lib._parsing._responses (a private SDK path) and the swap to responses.create(stream=True) dropped client-side output_parsed for structured-output streaming. The streaming-with-text_format path is the only one that does not surface the served-model header - documented inline. - Wrap the raw streaming responses in async with so the underlying socket closes deterministically (continuation_token retrieve + create paths). - Fix the empty-string / whitespace-only header at the source by stripping in _extract_served_model and returning None when nothing remains. - Revert unrelated formatting-only churn in _skills.py and test_mcp.py. - Update unit tests to assert against chat_response.model / update.model and add an aggregated streaming assertion plus a pin that the streaming-with-text_format path does not get the header. Verified end-to-end against Azure OpenAI Responses API: deployment alias gpt-5-nano now reports gpt-5-nano-2025-08-07 as ChatResponse.model in both the non-streaming and streaming paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: preserve streaming structured output finalization Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f62076ef-558d-49e8-8fe2-f38d527c9639 Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * refactor: name streaming response finalizer Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f62076ef-558d-49e8-8fe2-f38d527c9639 Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * fix: capture streaming response format after prepare Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f62076ef-558d-49e8-8fe2-f38d527c9639 Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * refactor: clarify streaming response format capture Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f62076ef-558d-49e8-8fe2-f38d527c9639 Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * test: use public API for streaming structured output Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f62076ef-558d-49e8-8fe2-f38d527c9639 Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Inline the served-model header override at its two call sites The `_apply_served_model_header` helper was a 1-line wrapper around `_extract_served_model`. Inlining the `if served_model is not None: ...` matches the pattern already used in the streaming paths and folds the explanatory docstring onto `_extract_served_model` (which is now the single place that knows about the header). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Eduard van Valkenburg Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> --- .../packages/core/agent_framework/_skills.py | 11 +- .../packages/core/tests/core/test_agents.py | 9 +- python/packages/core/tests/core/test_mcp.py | 4 +- .../tests/foundry/test_foundry_chat_client.py | 26 +- .../agent_framework_openai/_chat_client.py | 116 ++++-- .../tests/openai/test_openai_chat_client.py | 359 +++++++++++++++++- 6 files changed, 471 insertions(+), 54 deletions(-) diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index ba550e7095..6268c00879 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -651,9 +651,7 @@ def _validate_compatibility(compatibility: str | None) -> None: ValueError: If the value exceeds the maximum allowed length. """ if compatibility is not None and len(compatibility) > MAX_COMPATIBILITY_LENGTH: - raise ValueError( - f"Skill compatibility must be {MAX_COMPATIBILITY_LENGTH} characters or fewer." - ) + raise ValueError(f"Skill compatibility must be {MAX_COMPATIBILITY_LENGTH} characters or fewer.") def _build_skill_content( @@ -733,6 +731,7 @@ class InlineSkill(Skill): instructions="Use this skill for DB tasks.", ) + @skill.resource def get_schema() -> str: return "CREATE TABLE ..." @@ -2613,11 +2612,7 @@ class FileSkillsSource(SkillsSource): # Reject absolute paths (check both POSIX and Windows-style roots # so validation is consistent regardless of the host OS) - if ( - os.path.isabs(directory) - or normalized.startswith("/") - or re.match(r"^[A-Za-z]:[/\\]", directory) - ): + if os.path.isabs(directory) or normalized.startswith("/") or re.match(r"^[A-Za-z]:[/\\]", directory): logger.warning( "Skipping directory '%s': absolute paths are not allowed.", directory, diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index c7b3d7860c..f8e460e127 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -2567,10 +2567,15 @@ async def test_shared_local_storage_cross_provider_responses_history_does_not_le responses_second.incomplete = None responses_second.output = [responses_text_item] + def _as_raw(resp: MagicMock) -> MagicMock: + resp.parse = MagicMock(return_value=resp) + resp.headers = {} + return resp + with patch.object( - responses_client.client.responses, + responses_client.client.responses.with_raw_response, "create", - side_effect=[responses_first, responses_second], + side_effect=[_as_raw(responses_first), _as_raw(responses_second)], ) as mock_responses_create: responses_result = await responses_agent.run("Find me a hotel in Paris", session=session) diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 0fc5867d79..aea479ff86 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -4227,9 +4227,7 @@ async def test_mcp_tool_call_tool_forwards_tool_list_meta(): self.session.call_tool = AsyncMock( return_value=types.CallToolResult(content=[types.TextContent(type="text", text="result")]) ) - self.session.list_prompts = AsyncMock( - return_value=types.ListPromptsResult(prompts=[]) - ) + self.session.list_prompts = AsyncMock(return_value=types.ListPromptsResult(prompts=[])) def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]: return None diff --git a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py index eb8ff5937e..8f069b7f6d 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py @@ -86,12 +86,28 @@ def _with_foundry_debug() -> Any: return decorator +def _as_raw(mock_response: MagicMock) -> MagicMock: + """Wrap ``mock_response`` so it looks like an OpenAI ``with_raw_response`` wrapper. + + The chat client now calls ``responses.with_raw_response.{create,parse}`` and then + ``.parse()`` on the returned wrapper to get the actual response payload, plus + ``.headers`` to surface the ``x-ms-served-model`` Azure header. + """ + mock_response.parse = MagicMock(return_value=mock_response) + mock_response.headers = {} + return mock_response + + def _make_mock_openai_client() -> MagicMock: client = MagicMock() client.default_headers = {} client.responses = MagicMock() client.responses.create = AsyncMock() client.responses.parse = AsyncMock() + client.responses.with_raw_response = MagicMock() + client.responses.with_raw_response.create = AsyncMock() + client.responses.with_raw_response.parse = AsyncMock() + client.responses.with_raw_response.retrieve = AsyncMock() client.files = MagicMock() client.files.create = AsyncMock() client.files.delete = AsyncMock() @@ -470,7 +486,7 @@ async def test_content_filter_exception() -> None: body={"error": {"code": "content_filter", "message": "Content filter error"}}, ) mock_error.code = "content_filter" - client.client.responses.create.side_effect = mock_error + client.client.responses.with_raw_response.create.side_effect = mock_error with pytest.raises(OpenAIContentFilterException) as exc_info: await client.get_response(messages=[Message(role="user", contents=["Test message"])]) @@ -494,7 +510,7 @@ async def test_response_format_parse_path() -> None: mock_parsed_response.usage = None mock_parsed_response.finish_reason = None mock_parsed_response.conversation = None - client.client.responses.parse = AsyncMock(return_value=mock_parsed_response) + client.client.responses.with_raw_response.parse = AsyncMock(return_value=_as_raw(mock_parsed_response)) response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], @@ -522,7 +538,7 @@ async def test_response_format_parse_path_with_conversation_id() -> None: mock_parsed_response.finish_reason = None mock_parsed_response.conversation = MagicMock() mock_parsed_response.conversation.id = "conversation_456" - client.client.responses.parse = AsyncMock(return_value=mock_parsed_response) + client.client.responses.with_raw_response.parse = AsyncMock(return_value=_as_raw(mock_parsed_response)) response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], @@ -562,7 +578,7 @@ async def test_response_format_dict_parse_path() -> None: mock_message_item.type = "message" mock_message_item.content = [mock_message_content] mock_response.output = [mock_message_item] - client.client.responses.create = AsyncMock(return_value=mock_response) + client.client.responses.with_raw_response.create = AsyncMock(return_value=_as_raw(mock_response)) response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], @@ -587,7 +603,7 @@ async def test_bad_request_error_non_content_filter() -> None: body={"error": {"code": "invalid_request", "message": "Invalid request"}}, ) mock_error.code = "invalid_request" - client.client.responses.parse = AsyncMock(side_effect=mock_error) + client.client.responses.with_raw_response.parse = AsyncMock(side_effect=mock_error) with pytest.raises(ChatClientException) as exc_info: await client.get_response( diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index 40d9063b12..8257678584 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -359,6 +359,14 @@ class RawOpenAIChatClient( # type: ignore[misc] STORES_BY_DEFAULT: ClassVar[bool] = True # type: ignore[reportIncompatibleVariableOverride, misc] SUPPORTS_RICH_FUNCTION_OUTPUT: ClassVar[bool] = True + # Azure OpenAI Responses API may include this header in responses naming the actual model that + # served the request (e.g. ``gpt-5-nano-2025-08-07``), which can differ from the deployment alias + # that the request was addressed to and that ``response.model`` reports. When present, we use it + # as the value of ``ChatResponse.model`` / ``ChatResponseUpdate.model`` so telemetry and callers + # see the actually served model. (Chat Completions API already returns the snapshot in + # ``response.model``, so this header only matters for the Responses API.) + SERVED_MODEL_HEADER: ClassVar[str] = "x-ms-served-model" + FILE_SEARCH_MAX_RESULTS: int = 50 @overload @@ -606,25 +614,40 @@ class RawOpenAIChatClient( # type: ignore[misc] function_call_ids: dict[int, tuple[str, str]] = {} seen_reasoning_delta_item_ids: set[str] = set() validated_options: dict[str, Any] | None = None + # Captured once request options are validated/prepared so the streaming finalizer can + # still parse the aggregated response into structured output after the stream completes. + response_format: Any | None = None + + def _finalize_with_captured_format(updates: Sequence[ChatResponseUpdate]) -> ChatResponse[Any]: + # ResponseStream only calls the finalizer after iterating or draining `_stream()`, + # so `response_format` has already been populated from the validated request state + # unless request setup failed before streaming began. + return self._finalize_response_updates(updates, response_format=response_format) async def _stream() -> AsyncIterable[ChatResponseUpdate]: - nonlocal validated_options + nonlocal response_format, validated_options if continuation_token is not None: # Resume a background streaming response by retrieving with stream=True client = self.client validated_options = await self._validate_options(options) + response_format = validated_options.get("response_format") try: - stream_response = await client.responses.retrieve( + raw_stream_response = await client.responses.with_raw_response.retrieve( continuation_token["response_id"], stream=True, ) - async for chunk in stream_response: - yield self._parse_chunk_from_openai( - chunk, - options=validated_options, - function_call_ids=function_call_ids, - seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, - ) + served_model = self._extract_served_model(raw_stream_response.headers) + async with raw_stream_response.parse() as stream_response: + async for chunk in stream_response: + update = self._parse_chunk_from_openai( + chunk, + options=validated_options, + function_call_ids=function_call_ids, + seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, + ) + if served_model is not None: + update.model = served_model + yield update except Exception as ex: self._handle_request_error(ex) else: @@ -633,8 +656,15 @@ class RawOpenAIChatClient( # type: ignore[misc] run_options, validated_options, ) = await self._prepare_request(messages, options) + response_format = validated_options.get("response_format") try: if "text_format" in run_options: + # The SDK's ``responses.stream(text_format=...)`` helper preserves + # client-side ``output_parsed`` partial parsing for structured outputs, + # but it does not expose the raw HTTP response (no ``x-ms-served-model`` + # access). We accept that trade-off: this single streaming path keeps + # the deployment alias as the reported model name. All other paths + # surface the served-model header. async with client.responses.stream(**run_options) as response: async for chunk in response: yield self._parse_chunk_from_openai( @@ -644,18 +674,25 @@ class RawOpenAIChatClient( # type: ignore[misc] seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, ) else: - async for chunk in await client.responses.create(stream=True, **run_options): - yield self._parse_chunk_from_openai( - chunk, - options=validated_options, - function_call_ids=function_call_ids, - seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, - ) + raw_create_response = await client.responses.with_raw_response.create( + stream=True, **run_options + ) + served_model = self._extract_served_model(raw_create_response.headers) + async with raw_create_response.parse() as stream_response: + async for chunk in stream_response: + update = self._parse_chunk_from_openai( + chunk, + options=validated_options, + function_call_ids=function_call_ids, + seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids, + ) + if served_model is not None: + update.model = served_model + yield update except Exception as ex: self._handle_request_error(ex) - response_format = validated_options.get("response_format") if validated_options else None - return self._build_response_stream(_stream(), response_format=response_format) + return ResponseStream(_stream(), finalizer=_finalize_with_captured_format) # Non-streaming async def _get_response() -> ChatResponse: @@ -664,10 +701,14 @@ class RawOpenAIChatClient( # type: ignore[misc] client = self.client validated_options = await self._validate_options(options) try: - response = await client.responses.retrieve(continuation_token["response_id"]) + raw_response = await client.responses.with_raw_response.retrieve(continuation_token["response_id"]) + response = raw_response.parse() except Exception as ex: self._handle_request_error(ex) chat_response = self._parse_response_from_openai(response, options=validated_options) + served_model = self._extract_served_model(raw_response.headers) + if served_model is not None: + chat_response.model = served_model # Once the background response completes, drop the continuation_token from # the caller's options dict. FunctionInvocationLayer reuses the same dict # across tool-loop iterations, so leaving it in place makes the next iteration @@ -680,15 +721,39 @@ class RawOpenAIChatClient( # type: ignore[misc] client, run_options, validated_options = await self._prepare_request(messages, options) try: if "text_format" in run_options: - response = await client.responses.parse(stream=False, **run_options) + raw_response = await client.responses.with_raw_response.parse(stream=False, **run_options) # type: ignore else: - response = await client.responses.create(stream=False, **run_options) + raw_response = await client.responses.with_raw_response.create(stream=False, **run_options) # type: ignore + response = raw_response.parse() except Exception as ex: self._handle_request_error(ex) - return self._parse_response_from_openai(response, options=validated_options) + chat_response = self._parse_response_from_openai(response, options=validated_options) + served_model = self._extract_served_model(raw_response.headers) + if served_model is not None: + chat_response.model = served_model + return chat_response return _get_response() + @classmethod + def _extract_served_model(cls, headers: Any) -> str | None: + """Return the Azure OpenAI ``x-ms-served-model`` response header value when present. + + Azure OpenAI Responses API returns the deployment alias in ``response.model`` but the actual + snapshot served via the ``x-ms-served-model`` response header (e.g. ``gpt-5-nano-2025-08-07`` + vs deployment alias ``gpt-5-nano``). When present, the served snapshot is the source of truth + for observability and downstream callers. Empty/whitespace-only header values are rejected + here so every caller can simply check ``if served_model is not None``. + """ + if headers is None: + return None + served_model = headers.get(cls.SERVED_MODEL_HEADER) + if isinstance(served_model, str): + stripped = served_model.strip() + if stripped: + return stripped + return None + def _prepare_response_and_text_format( self, *, @@ -1429,9 +1494,10 @@ class RawOpenAIChatClient( # type: ignore[misc] props = content.additional_properties or {} # Local-shell variant serializes as `local_shell_call` carrying a server-issued id; # plain function_call_output pairs by call_id and is safe under storage. - if ( - props.get(OPENAI_SHELL_OUTPUT_TYPE_KEY) == OPENAI_SHELL_OUTPUT_TYPE_LOCAL_SHELL_CALL - and props.get(OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY) + if props.get( + OPENAI_SHELL_OUTPUT_TYPE_KEY + ) == OPENAI_SHELL_OUTPUT_TYPE_LOCAL_SHELL_CALL and props.get( + OPENAI_LOCAL_SHELL_CALL_ITEM_ID_KEY ): continue new_args: dict[str, Any] = {} diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 325986a730..31c3c26fe0 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -72,9 +72,10 @@ class OutputStruct(BaseModel): class _FakeAsyncEventStream: - def __init__(self, events: list[object]) -> None: + def __init__(self, events: list[object], headers: dict[str, str] | None = None) -> None: self._events = events self._iterator = iter(()) + self._headers = headers or {} def __aiter__(self) -> "_FakeAsyncEventStream": self._iterator = iter(self._events) @@ -86,6 +87,45 @@ class _FakeAsyncEventStream: except StopIteration as exc: raise StopAsyncIteration from exc + # The chat client now consumes the streaming response via ``with_raw_response``, + # which returns a wrapper exposing ``.parse()`` (the underlying iterable) and + # ``.headers``. The chat client then ``async with``-s the parsed stream so the + # underlying socket is closed deterministically. Mimic both interfaces here so + # test mocks remain a single object. + def parse(self) -> "_FakeAsyncEventStream": + return self + + @property + def headers(self) -> dict[str, str]: + return self._headers + + async def __aenter__(self) -> "_FakeAsyncEventStream": + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + traceback: object | None, + ) -> None: + return None + + +def _as_raw(mock_response: MagicMock, *, headers: dict[str, str] | None = None) -> MagicMock: + """Make ``mock_response`` look like an OpenAI ``with_raw_response`` wrapper. + + The chat client now calls ``responses.with_raw_response.{create,parse,retrieve}`` + and then ``.parse()`` on the returned wrapper to get the actual response payload, + plus ``.headers`` to surface the ``x-ms-served-model`` Azure header. Tests still + patch the underlying ``responses.{create,parse,retrieve}`` methods (the SDK's + raw-response wrapper internally delegates to these), so the patched return value + is what our code unwraps. Setting ``mock_response.parse`` to return the mock + itself lets the existing assertions on ``mock_response.id`` etc. continue to work. + """ + mock_response.parse = MagicMock(return_value=mock_response) + mock_response.headers = headers or {} + return mock_response + class _FakeAsyncEventStreamContext(_FakeAsyncEventStream): async def __aenter__(self) -> "_FakeAsyncEventStreamContext": @@ -477,7 +517,7 @@ async def test_response_format_parse_path() -> None: mock_parsed_response.finish_reason = None mock_parsed_response.conversation = None # No conversation object - with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): + with patch.object(client.client.responses, "parse", return_value=_as_raw(mock_parsed_response)): response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], options={"response_format": OutputStruct, "store": True}, @@ -504,7 +544,7 @@ async def test_response_format_parse_path_with_conversation_id() -> None: mock_parsed_response.conversation = MagicMock() mock_parsed_response.conversation.id = "conversation_456" - with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): + with patch.object(client.client.responses, "parse", return_value=_as_raw(mock_parsed_response)): response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], options={"response_format": OutputStruct, "store": True}, @@ -542,7 +582,7 @@ async def test_response_format_dict_parse_path() -> None: mock_message_item.content = [mock_message_content] mock_response.output = [mock_message_item] - with patch.object(client.client.responses, "create", return_value=mock_response): + with patch.object(client.client.responses, "create", return_value=_as_raw(mock_response)): response = await client.get_response( messages=[Message(role="user", contents=["Test message"])], options={"response_format": response_format}, @@ -554,6 +594,297 @@ async def test_response_format_dict_parse_path() -> None: assert response.value["answer"] == "Parsed" +_SERVED_MODEL_HEADER = "x-ms-served-model" + + +async def test_served_model_header_overrides_response_model() -> None: + """The ``x-ms-served-model`` Azure response header should overwrite ChatResponse.model.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.id = "response_123" + mock_response.model = "test-model" # deployment alias returned in the body + mock_response.created_at = 1000000000 + mock_response.metadata = {} + mock_response.output_parsed = None + mock_response.output = [] + mock_response.usage = None + mock_response.finish_reason = None + mock_response.conversation = None + mock_response.status = "completed" + + raw = _as_raw(mock_response, headers={_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"}) + + with patch.object(client.client.responses, "create", return_value=raw): + response = await client.get_response( + messages=[Message(role="user", contents=["Test message"])], + ) + + assert response.model == "gpt-4o-2024-08-06" + + +async def test_served_model_header_absent_keeps_response_model() -> None: + """When the served-model header is missing ChatResponse.model should come from the response body.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.id = "response_123" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.metadata = {} + mock_response.output_parsed = None + mock_response.output = [] + mock_response.usage = None + mock_response.finish_reason = None + mock_response.conversation = None + mock_response.status = "completed" + + # _as_raw sets headers to {} by default — i.e. no x-ms-served-model. + with patch.object(client.client.responses, "create", return_value=_as_raw(mock_response)): + response = await client.get_response( + messages=[Message(role="user", contents=["Test message"])], + ) + + assert response.model == "test-model" + + +async def test_served_model_header_empty_string_does_not_override() -> None: + """Empty/whitespace header values should not overwrite the response body's model name.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_response = MagicMock() + mock_response.id = "response_123" + mock_response.model = "test-model" + mock_response.created_at = 1000000000 + mock_response.metadata = {} + mock_response.output_parsed = None + mock_response.output = [] + mock_response.usage = None + mock_response.finish_reason = None + mock_response.conversation = None + mock_response.status = "completed" + + raw = _as_raw(mock_response, headers={_SERVED_MODEL_HEADER: " "}) + + with patch.object(client.client.responses, "create", return_value=raw): + response = await client.get_response( + messages=[Message(role="user", contents=["Test message"])], + ) + + assert response.model == "test-model" + + +async def test_served_model_header_captured_on_parse_path() -> None: + """The served-model header should also be captured on the structured-output (parse) path.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + mock_parsed_response = MagicMock() + mock_parsed_response.id = "parsed_response_123" + mock_parsed_response.text = "Parsed response" + mock_parsed_response.model = "test-model" + mock_parsed_response.created_at = 1000000000 + mock_parsed_response.metadata = {} + mock_parsed_response.output_parsed = None + mock_parsed_response.usage = None + mock_parsed_response.finish_reason = None + mock_parsed_response.conversation = None + + raw = _as_raw(mock_parsed_response, headers={_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"}) + + with patch.object(client.client.responses, "parse", return_value=raw): + response = await client.get_response( + messages=[Message(role="user", contents=["Test message"])], + options={"response_format": OutputStruct, "store": True}, + ) + + assert response.model == "gpt-4o-2024-08-06" + + +async def test_served_model_header_propagated_to_streaming_updates() -> None: + """In streaming mode the served-model header should overwrite update.model on every chunk.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + events = [ + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=1, + logprobs=[], + delta="Hello", + ), + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=2, + logprobs=[], + delta=" world", + ), + ] + + fake_stream = _FakeAsyncEventStream(events, headers={_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"}) + + with ( + patch.object(client, "_prepare_request", new=AsyncMock(return_value=(client.client, {}, {}))), + patch.object(client.client.responses, "create", new=AsyncMock(return_value=fake_stream)), + patch.object(client, "_get_metadata_from_response", return_value={}), + ): + stream = client._inner_get_response(messages=[Message(role="user", contents=["Hi"])], options={}, stream=True) + updates = [update async for update in stream] + + assert updates, "Expected at least one streaming update" + for update in updates: + assert update.model == "gpt-4o-2024-08-06" + + +async def test_served_model_header_aggregates_into_final_streaming_response() -> None: + """Aggregating updates via to_chat_response() should preserve the served-model value.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + events = [ + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=1, + logprobs=[], + delta="Hello", + ), + ] + + fake_stream = _FakeAsyncEventStream(events, headers={_SERVED_MODEL_HEADER: "gpt-4o-2024-08-06"}) + + with ( + patch.object(client, "_prepare_request", new=AsyncMock(return_value=(client.client, {}, {}))), + patch.object(client.client.responses, "create", new=AsyncMock(return_value=fake_stream)), + patch.object(client, "_get_metadata_from_response", return_value={}), + ): + stream = client._inner_get_response(messages=[Message(role="user", contents=["Hi"])], options={}, stream=True) + updates = [update async for update in stream] + + final = ChatResponse.from_updates(updates) + assert final.model == "gpt-4o-2024-08-06" + + +async def test_served_model_header_absent_in_streaming_updates() -> None: + """When the header is missing in streaming mode update.model should fall back to the deployment alias.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + events = [ + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=1, + logprobs=[], + delta="Hello", + ), + ] + + fake_stream = _FakeAsyncEventStream(events) # default empty headers + + with ( + patch.object(client, "_prepare_request", new=AsyncMock(return_value=(client.client, {}, {}))), + patch.object(client.client.responses, "create", new=AsyncMock(return_value=fake_stream)), + patch.object(client, "_get_metadata_from_response", return_value={}), + ): + stream = client._inner_get_response(messages=[Message(role="user", contents=["Hi"])], options={}, stream=True) + updates = [update async for update in stream] + + assert updates, "Expected at least one streaming update" + for update in updates: + # Without the header, _parse_chunk_from_openai's default is the client's model name. + assert update.model == "test-model" + + +async def test_served_model_header_not_captured_for_streaming_text_format() -> None: + """The streaming structured-output path uses ``responses.stream(...)`` and therefore cannot + surface the served-model header. Pin this behavior so any future change is intentional.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + events = [ + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=1, + logprobs=[], + delta="Hello", + ), + ] + + # `responses.stream(...)` returns an async context manager. The headers attribute + # is irrelevant because this code path never asks for it. + fake_stream_ctx = _FakeAsyncEventStreamContext(events) + + with ( + patch.object( + client, + "_prepare_request", + new=AsyncMock(return_value=(client.client, {"text_format": OutputStruct}, {})), + ), + patch.object(client.client.responses, "stream", return_value=fake_stream_ctx), + patch.object(client, "_get_metadata_from_response", return_value={}), + ): + stream = client._inner_get_response(messages=[Message(role="user", contents=["Hi"])], options={}, stream=True) + updates = [update async for update in stream] + + assert updates, "Expected at least one streaming update" + for update in updates: + # No header override; model stays the deployment alias. + assert update.model == "test-model" + + +async def test_streaming_text_format_preserves_final_structured_output() -> None: + """Streaming structured output should still parse into the final ChatResponse value.""" + client = OpenAIChatClient(model="test-model", api_key="test-key") + + events = [ + ResponseTextDeltaEvent( + type="response.output_text.delta", + content_index=0, + item_id="text_item", + output_index=0, + sequence_number=1, + logprobs=[], + delta='{"location":"Seattle","weather":"Sunny"}', + ), + ] + + fake_stream_ctx = _FakeAsyncEventStreamContext(events) + + with ( + patch.object( + client, + "_prepare_request", + new=AsyncMock( + return_value=( + client.client, + {"text_format": OutputStruct}, + {"response_format": OutputStruct}, + ) + ), + ), + patch.object(client.client.responses, "stream", return_value=fake_stream_ctx), + patch.object(client, "_get_metadata_from_response", return_value={}), + ): + stream = client.get_response( + messages=[Message(role="user", contents=["Hi"])], + options={"response_format": OutputStruct}, + stream=True, + ) + response = await stream.get_final_response() + + assert response.model == "test-model" + assert response.value == OutputStruct(location="Seattle", weather="Sunny") + + async def test_bad_request_error_non_content_filter() -> None: """Test get_response BadRequestError without content_filter.""" client = OpenAIChatClient(model="test-model", api_key="test-key") @@ -953,7 +1284,9 @@ async def test_local_shell_tool_is_invoked_in_function_loop() -> None: mock_text_item.content = [mock_text_content] mock_response2.output = [mock_text_item] - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: await client.get_response( messages=[Message(role="user", contents=["What Python version is available?"])], options={"tools": [local_shell_tool]}, @@ -1026,7 +1359,9 @@ async def test_shell_call_is_invoked_as_local_shell_function_loop() -> None: mock_text_item.content = [mock_text_content] mock_response2.output = [mock_text_item] - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: await client.get_response( messages=[Message(role="user", contents=["What Python version is available?"])], options={"tools": [local_shell_tool]}, @@ -1097,7 +1432,9 @@ async def test_tool_loop_store_false_omits_reasoning_items_from_second_request() mock_text_item.content = [mock_text_content] mock_response2.output = [mock_text_item] - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: response = await client.get_response( messages=[Message(role="user", contents=["What's the weather in Amsterdam?"])], options={ @@ -2810,7 +3147,9 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: mock_response2.output = [mock_text_item] # Patch the create call to return the two mocked responses in sequence - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[_as_raw(mock_response1), _as_raw(mock_response2)] + ) as mock_create: # First call: get the approval request response = await client.get_response(messages=[Message(role="user", contents=["Trigger approval"])]) assert response.messages[0].contents[0].type == "function_approval_request" @@ -4120,9 +4459,7 @@ async def test_prepare_options_with_conversation_id_strips_server_items_for_mixe types = [item.get("type") for item in options["input"]] assert "reasoning" not in types assert "function_call" not in types - output_call_ids = { - item["call_id"] for item in options["input"] if item.get("type") == "function_call_output" - } + output_call_ids = {item["call_id"] for item in options["input"] if item.get("type") == "function_call_output"} assert output_call_ids == {"call_history", "call_live"} assert options["previous_response_id"] == "resp_prev123" From 66a09a76afc788280dd2a02aee881bbc397fe6ac Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Tue, 19 May 2026 13:41:53 +0200 Subject: [PATCH 03/22] Python: fix: hyperlight skips symlinks when staging sandbox input (#5919) * Python: fix(hyperlight): skip symlinks when staging files into the sandbox The helpers that populate the sandbox input tree (``_copy_path`` and the ``_path_tree_signature`` walker used for cache invalidation) relied on ``Path.is_file()``, ``Path.is_dir()`` and ``shutil.copy2`` - all of which follow symlinks by default. When the source tree contains symlinks, that let entries from outside the configured input source surface inside the sandbox. Harden both code paths to never follow symlinks: - ``_copy_path`` now bails out via ``Path.is_symlink()`` before any ``is_dir()`` / ``is_file()`` check, skips non-regular files, and uses ``shutil.copy2(..., follow_symlinks=False)`` as defense in depth. - New ``_iter_real_entries`` walker replaces the previous ``Path.rglob`` call inside ``_path_tree_signature`` (rglob follows directory symlinks). - ``_path_tree_signature`` switches to ``Path.lstat()`` so size/mtime are never read through a symlink target. Added regression tests covering: - A pre-placed file symlink in ``workspace_root`` (top level). - A pre-placed directory symlink in ``workspace_root``. - A nested file symlink inside a real subdirectory. - ``_path_tree_signature`` ignoring symlinks so the cache key reflects only what is actually staged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix(hyperlight): address PR #5919 review feedback - _iter_real_entries now yields directories and regular files only, skipping non-regular entries (sockets/FIFOs/devices). Keeps the cache-key signature consistent with what _copy_path actually stages. - The four new symlink regression tests skip when the platform does not support symlink creation (e.g. unprivileged Windows runners), via a local _symlinks_supported helper modelled on the one in packages/core/tests/core/test_skills.py. Prevents OSError / NotImplementedError from failing CI jobs that have nothing to do with the change under test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix(hyperlight): address PR #5919 follow-up review feedback - _copy_path docstring: narrow the scope to "symlink entries present in the source tree at rest" and explicitly call out that the copy is NOT atomic with respect to concurrent mutation of the source tree. Callers who need that stronger guarantee should snapshot their workspace before passing it in. Avoids overpromising on a TOCTOU window that pathlib cannot express; closing it properly would need fd-based traversal (O_NOFOLLOW | O_DIRECTORY + os.scandir(fd)) with a separate Windows story, which is out of scope for this targeted fix. - _path_tree_signature: drop the `if path.is_symlink(): return ()` short-circuit. Resolve a symlink root to its real target before walking instead. The public construction flow already resolves workspace_root / file_mounts[].host_path up front so this never affected user-facing code, but the short-circuit was misleading and would have produced an empty, stable signature for any direct caller that builds a _RunConfig without going through the public constructor. Defense in depth: even if a future call site forgets to resolve the root, the cache key still reflects real contents. - Added regression test test_path_tree_signature_walks_through_symlinked_root: a symlinked workspace root must produce a non-empty signature, AND the signature must change when the real target's contents change so the cache key actually invalidates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../_execute_code_tool.py | 84 ++++++++- .../hyperlight/test_hyperlight_codeact.py | 169 ++++++++++++++++++ 2 files changed, 248 insertions(+), 5 deletions(-) diff --git a/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py b/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py index 582cbece22..738b080183 100644 --- a/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py +++ b/python/packages/hyperlight/agent_framework_hyperlight/_execute_code_tool.py @@ -7,7 +7,7 @@ import mimetypes import shutil import threading import time -from collections.abc import Callable, Sequence +from collections.abc import Callable, Iterator, Sequence from concurrent.futures import ThreadPoolExecutor from contextlib import suppress from copy import copy @@ -483,15 +483,66 @@ def _display_mount_path(mount_path: str) -> str: return f"/input/{mount_path}" +def _iter_real_entries(root: Path) -> Iterator[Path]: + """Walk ``root`` recursively, yielding directories and regular files only. + + ``Path.rglob`` follows directory symlinks by default, which combined with + ``Path.is_file()`` / ``shutil.copy2`` (all follow symlinks) would expose + paths outside the configured input tree if the source tree is + attacker-controlled. This walker mirrors the safe behaviour by checking + ``is_symlink()`` at every directory level and never descending through one. + + Non-regular files (sockets, FIFOs, devices) are also filtered out so the + signature mirrors exactly what ``_copy_path`` actually stages. + """ + stack: list[Path] = [root] + while stack: + current = stack.pop() + try: + children = list(current.iterdir()) + except OSError: + continue + for child in children: + try: + if child.is_symlink(): + continue + if child.is_dir(): + stack.append(child) + yield child + elif child.is_file(): + yield child + # Non-regular files (sockets/FIFOs/devices) are skipped to + # match ``_copy_path``'s staging behaviour. + except OSError: + continue + + def _path_tree_signature(path: Path) -> tuple[tuple[str, int, int], ...]: + """Return a stable signature of the real (non-symlink) file tree under ``path``. + + If ``path`` itself is a symlink, it is resolved first so the signature + reflects the real target's contents. This matches the public construction + flow (``_resolve_workspace_root`` / ``_normalize_file_mount_input`` already + resolve roots up front) and acts as defense in depth for any direct caller + that builds a ``_RunConfig`` without going through the constructor. + + Symlinks encountered inside the walked tree are skipped, and ``lstat()`` is + used so size/mtime are read from the entry itself, never through a + target. The result mirrors what ``_copy_path`` actually stages. + """ + if path.is_symlink(): + try: + path = path.resolve(strict=True) + except OSError: + return () if path.is_file(): - stat = path.stat() + stat = path.lstat() return ((path.name, int(stat.st_size), int(stat.st_mtime_ns)),) entries: list[tuple[str, int, int]] = [] - for candidate in sorted(path.rglob("*"), key=lambda value: value.as_posix()): + for candidate in sorted(_iter_real_entries(path), key=lambda value: value.as_posix()): try: - stat = candidate.stat() + stat = candidate.lstat() except FileNotFoundError: continue relative_path = candidate.relative_to(path).as_posix() @@ -501,14 +552,37 @@ def _path_tree_signature(path: Path) -> tuple[tuple[str, int, int], ...]: def _copy_path(source: Path, destination: Path) -> None: + """Stage ``source`` into ``destination`` without following symlinks. + + Symlinks (file or directory) found in the source tree are skipped entirely + so a sandbox input tree can only contain real entries that physically live + under the configured ``workspace_root`` or a ``file_mounts`` host path. + ``Path.is_dir()``, ``Path.is_file()`` and ``shutil.copy2`` all follow + symlinks by default, which is unsafe for symlinks planted in the source + tree at rest. + + This helper does not attempt to make the copy atomic with respect to + concurrent mutation of the source tree. Callers that need protection from + an adversary modifying the workspace mid-stage should pass in an + immutable / snapshotted directory. + """ + # Detect symlinks before doing anything else - ``is_symlink()`` does not + # follow the link, unlike ``is_dir()`` / ``is_file()``. + if source.is_symlink(): + return + if source.is_dir(): destination.mkdir(parents=True, exist_ok=True) for child in sorted(source.iterdir(), key=lambda value: value.name): _copy_path(child, destination / child.name) return + if not source.is_file(): + # Non-regular files (sockets, FIFOs, devices) are intentionally skipped. + return + destination.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(source, destination) + shutil.copy2(source, destination, follow_symlinks=False) def _populate_input_dir(*, config: _RunConfig, input_root: Path) -> None: diff --git a/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py b/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py index 9611978744..03e3c2269c 100644 --- a/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py +++ b/python/packages/hyperlight/tests/hyperlight/test_hyperlight_codeact.py @@ -425,6 +425,175 @@ async def test_execute_code_tool_populates_input_dir_with_workspace_and_file_mou assert (input_root / "data" / "input.txt").read_text(encoding="utf-8") == "hello from mount" +def _build_run_config( + *, + workspace_root: Path | None = None, + file_mounts: tuple = (), +) -> Any: + """Build a minimal _RunConfig for tests that exercise _populate_input_dir directly.""" + return execute_code_module._RunConfig( + backend="wasm", + module="python_guest.path", + module_path=None, + approval_mode="never_require", + tools=(), + workspace_root=workspace_root, + workspace_signature=(), + file_mounts=file_mounts, + allowed_domains=(), + ) + + +def _symlinks_supported(tmp: Path) -> bool: + """Return True if the current platform/environment supports symlinks. + + Mirrors python/packages/core/tests/core/test_skills.py so the symlink + regression tests are skipped on restricted Windows CI runners instead of + failing on ``OSError`` / ``NotImplementedError`` during creation. + """ + test_target = tmp / "_symlink_test_target" + test_link = tmp / "_symlink_test_link" + try: + test_target.write_text("test", encoding="utf-8") + test_link.symlink_to(test_target) + return True + except (OSError, NotImplementedError): + return False + finally: + test_link.unlink(missing_ok=True) + test_target.unlink(missing_ok=True) + + +def test_populate_input_dir_skips_symlink_to_file_outside_workspace(tmp_path: Path) -> None: + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside.txt" + outside.write_text("outside-content", encoding="utf-8") + (workspace / "real.txt").write_text("real-content", encoding="utf-8") + (workspace / "link.txt").symlink_to(outside) + + input_root = tmp_path / "input" + input_root.mkdir() + + execute_code_module._populate_input_dir( + config=_build_run_config(workspace_root=workspace), + input_root=input_root, + ) + + # Real file copied; symlink and its target are absent. + assert (input_root / "real.txt").read_text(encoding="utf-8") == "real-content" + assert not (input_root / "link.txt").exists() + assert not (input_root / "link.txt").is_symlink() + # Sanity: no outside-content anywhere in the input tree. + leaked = [ + path + for path in input_root.rglob("*") + if path.is_file() and path.read_text(encoding="utf-8") == "outside-content" + ] + assert leaked == [] + + +def test_populate_input_dir_skips_symlinked_directory_outside_workspace(tmp_path: Path) -> None: + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + workspace = tmp_path / "workspace" + workspace.mkdir() + outside_dir = tmp_path / "outside_dir" + outside_dir.mkdir() + (outside_dir / "deep.txt").write_text("deep-content", encoding="utf-8") + (workspace / "linked_dir").symlink_to(outside_dir, target_is_directory=True) + + input_root = tmp_path / "input" + input_root.mkdir() + + execute_code_module._populate_input_dir( + config=_build_run_config(workspace_root=workspace), + input_root=input_root, + ) + + # Neither the symlink itself nor anything under the symlinked target leaks. + assert not (input_root / "linked_dir").exists() + leaked = [ + path for path in input_root.rglob("*") if path.is_file() and path.read_text(encoding="utf-8") == "deep-content" + ] + assert leaked == [] + + +def test_populate_input_dir_skips_nested_symlinks(tmp_path: Path) -> None: + """A symlink several levels deep inside a real subdir must also be skipped.""" + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + workspace = tmp_path / "workspace" + (workspace / "real_sub").mkdir(parents=True) + (workspace / "real_sub" / "ok.txt").write_text("ok", encoding="utf-8") + outside = tmp_path / "outside.txt" + outside.write_text("outside-content", encoding="utf-8") + (workspace / "real_sub" / "link.txt").symlink_to(outside) + + input_root = tmp_path / "input" + input_root.mkdir() + + execute_code_module._populate_input_dir( + config=_build_run_config(workspace_root=workspace), + input_root=input_root, + ) + + assert (input_root / "real_sub" / "ok.txt").read_text(encoding="utf-8") == "ok" + assert not (input_root / "real_sub" / "link.txt").exists() + + +def test_path_tree_signature_does_not_follow_symlinks(tmp_path: Path) -> None: + """The cache-key signature must reflect only real files (mirrors the staged tree).""" + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + workspace = tmp_path / "workspace" + workspace.mkdir() + real = workspace / "real.txt" + real.write_text("real-content", encoding="utf-8") + outside = tmp_path / "outside.txt" + outside.write_text("outside-content", encoding="utf-8") + (workspace / "link.txt").symlink_to(outside) + + signature = execute_code_module._path_tree_signature(workspace) + + names = [entry[0] for entry in signature] + assert "real.txt" in names + assert "link.txt" not in names + + +def test_path_tree_signature_walks_through_symlinked_root(tmp_path: Path) -> None: + """A symlinked workspace root must produce a real signature, not an empty one. + + Defends against the cache never invalidating when a caller passes a + symlinked workspace and the underlying real directory's contents change. + """ + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + + real_workspace = tmp_path / "real_workspace" + real_workspace.mkdir() + target = real_workspace / "data.txt" + target.write_text("v1", encoding="utf-8") + + linked_workspace = tmp_path / "linked_workspace" + linked_workspace.symlink_to(real_workspace, target_is_directory=True) + + signature_v1 = execute_code_module._path_tree_signature(linked_workspace) + names = [entry[0] for entry in signature_v1] + assert "data.txt" in names, f"signature should include the target's contents, got {signature_v1!r}" + + # Mutate the real contents; the symlinked-root signature must reflect the change + # so the cache key invalidates. + import time + + time.sleep(0.01) # ensure mtime_ns moves on filesystems with coarse granularity + target.write_text("v2-content-larger", encoding="utf-8") + signature_v2 = execute_code_module._path_tree_signature(linked_workspace) + assert signature_v1 != signature_v2, "signature should change when symlinked target contents change" + + def test_execute_code_tool_allowed_domains_use_structured_entries_and_replace_by_target() -> None: execute_code = HyperlightExecuteCodeTool(_registry=_FakeRuntime()) From 3f522a824625fd3ae5f3decedee3cabaa6100c5c Mon Sep 17 00:00:00 2001 From: Taisir Hassan Date: Tue, 19 May 2026 07:02:20 -0700 Subject: [PATCH 04/22] Remove duplicate pop in InMemoryCacheProvider.remove (#5795) The second self._cache.pop(key, None) call is a guaranteed no-op: the first pop has already removed the key (or returned None), and there is no await between the two statements that could allow another coroutine to re-add it. Removing the dead line clarifies intent without changing behavior. --- python/packages/purview/agent_framework_purview/_cache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/packages/purview/agent_framework_purview/_cache.py b/python/packages/purview/agent_framework_purview/_cache.py index d559895a63..df5d03b97e 100644 --- a/python/packages/purview/agent_framework_purview/_cache.py +++ b/python/packages/purview/agent_framework_purview/_cache.py @@ -161,7 +161,6 @@ class InMemoryCacheProvider: entry = self._cache.pop(key, None) if entry is not None: self._current_size_bytes -= entry[2] - self._cache.pop(key, None) def create_protection_scopes_cache_key(request: ProtectionScopesRequest) -> str: From 8ccaf7fb825dd64abd4371aef9ce7ff3c2c6c599 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 19 May 2026 16:32:14 +0100 Subject: [PATCH 05/22] Harness Console: Add a factory option for creating custom sessions (#5951) --- .../Harness/Harness_Shared_Console/HarnessConsole.cs | 4 +++- .../Harness/Harness_Shared_Console/HarnessConsoleOptions.cs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs index 1f313d1008..83491a7739 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsole.cs @@ -33,7 +33,9 @@ public static class HarnessConsole var modeProvider = agent.GetService(); var messageInjector = agent.GetService(); - AgentSession session = await agent.CreateSessionAsync(); + AgentSession session = options.SessionFactory is not null + ? await options.SessionFactory(agent) + : await agent.CreateSessionAsync(); using var component = new HarnessAppComponent( placeholder: userPrompt, diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsoleOptions.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsoleOptions.cs index eedb4f36b3..9c582f390c 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsoleOptions.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessConsoleOptions.cs @@ -45,6 +45,12 @@ public class HarnessConsoleOptions /// public Dictionary ModeColors { get; set; } = new(DefaultModeColors, StringComparer.OrdinalIgnoreCase); + /// + /// Gets or sets an optional factory for creating the . + /// When (the default), is used. + /// + public Func>? SessionFactory { get; set; } + /// /// Creates the default set of observers without planning support. /// Includes tool call display, tool approval, error display, reasoning display, From afcb6b1a007728f481495cd4cb26a5cb4b87de8a Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 19 May 2026 16:49:03 +0100 Subject: [PATCH 06/22] .NET: Harness code act skill sample (#5930) * Add sample that shows code execution and skills together * Use nuget for python module path * Update readme. * Fix formatting. * Reduce flashing in rendering. * Improve screen clearing for Powershell * Add a couple of small UX fixes --- dotnet/Directory.Packages.props | 1 + dotnet/agent-framework-dotnet.slnx | 1 + .../ConsoleReactiveComponents/AnsiEscapes.cs | 5 + .../Components/AgentStatus.cs | 8 +- .../Harness_Shared_Console/HarnessConsole.cs | 4 + .../Harness_Step04_CodeExecution.csproj | 29 +++++ .../Harness_Step04_CodeExecution/Program.cs | 122 ++++++++++++++++++ .../Harness_Step04_CodeExecution/README.md | 51 ++++++++ .../skills/regex-tester/SKILL.md | 36 ++++++ .../references/regex-cheatsheet.md | 97 ++++++++++++++ 10 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Harness_Step04_CodeExecution.csproj create mode 100644 dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs create mode 100644 dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md create mode 100644 dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/SKILL.md create mode 100644 dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/references/regex-cheatsheet.md diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 2121be9207..efa9a70227 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -112,6 +112,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index d494768139..d694c10cbc 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -124,6 +124,7 @@ + diff --git a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs index b13c2ddc82..cf916938e7 100644 --- a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs +++ b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/AnsiEscapes.cs @@ -24,6 +24,11 @@ public static class AnsiEscapes /// public static string MoveCursor(int row, int column) => $"\x1b[{row};{column}H"; + /// + /// Erases the current line from the cursor position to the end of the line (EL 0). + /// + public static string EraseToEndOfLine => "\x1b[0K"; + /// /// Erases the entire current line (EL 2). /// diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs index f07035d27a..725e1ffaa6 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentStatus.cs @@ -35,6 +35,7 @@ public class AgentStatus : ConsoleReactiveComponent /// Initializes a new instance of the class. @@ -85,7 +86,12 @@ public class AgentStatus : ConsoleReactiveComponent + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs new file mode 100644 index 0000000000..af53443c63 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates a HarnessAgent with ALL features enabled, plus: +// - Hyperlight CodeAct (HyperlightCodeActProvider) for sandboxed Python code execution +// - Skills (AgentSkillsProvider) discovering a local "regex-tester" skill +// +// The agent can plan tasks with todos, manage modes, store memories, read/write files, +// search the web, approve sensitive tools, discover and use skills, and execute arbitrary +// Python code in a Hyperlight sandbox — all pre-configured by the HarnessAgent. +// +// Try asking: "Help me write a regex that matches valid email addresses, then test it." +// +// Special commands: +// /todos — Display the current todo list without invoking the agent. +// /mode — Get or set the current agent mode. +// /exit — End the session. + +#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage. +#pragma warning disable MAAI001 // Suppress experimental API warnings for Agents AI experiments. + +using System.ClientModel.Primitives; +using Azure.AI.Projects; +using Azure.Identity; +using Harness.Shared.Console; +using HyperlightSandbox.Guest.Python; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hyperlight; +using Microsoft.Extensions.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4"; + +const int MaxContextWindowTokens = 1_050_000; +const int MaxOutputTokens = 128_000; +const string TracingSourceName = "Harness.CodeExecution"; + +// Set up OpenTelemetry tracing that writes spans to a text file. +using var tracerProvider = HarnessTracing.CreateFileTracerProvider(TracingSourceName); + +// Create the HyperlightCodeActProvider with the Python/Wasm backend. +// The guest module path is resolved automatically from the Hyperlight.HyperlightSandbox.Guest.Python NuGet package. +using var codeAct = new HyperlightCodeActProvider( + HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath())); + +var instructions = + """ + ## Technical Assistant Instructions + + You are a code-powered technical assistant. You can execute Python code in a sandboxed environment + to solve problems precisely rather than guessing. You also have access to skills that provide + structured workflows for specific technical tasks. + + ### Code Execution + + When a problem requires computation, validation, or testing: + - Write Python code and use `execute_code` to run it in the sandbox. + - Always verify results by running the code rather than reasoning about what would happen. + - If code fails, read the error message carefully, fix the issue, and retry. + + ### Skills + + You have access to discoverable skills. When a task matches a skill's description: + - Follow the skill's instructions carefully. + - Use the skill's reference materials for context. + - Combine the skill's workflow with code execution when appropriate. + + ### Planning and Research + + For complex tasks: + - Break the problem into steps using your todo list. + - Research background information using web search when needed. + - Save important findings to file memory for later reference. + + ### Presenting Results + + - Show your work: include the code you ran and its output. + - Explain what each part of your solution does. + - If applicable, save final results to file memory. + """; + +// Create the agent with ALL HarnessAgent features enabled plus Hyperlight CodeAct. +// No Disable* flags are set — TodoProvider, AgentModeProvider, FileMemory, FileAccess, +// ToolApproval, WebSearch, and AgentSkillsProvider are all active. +AIAgent agent = + new AIProjectClient( + new Uri(endpoint), + new DefaultAzureCredential(), + new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) }) + .GetProjectOpenAIClient() + .GetResponsesClient() + .AsIChatClient(deploymentName) + .AsHarnessAgent(MaxContextWindowTokens, MaxOutputTokens, new HarnessAgentOptions + { + Name = "CodeExecutionAgent", + Description = "A technical assistant with sandboxed code execution and skill-based workflows.", + OpenTelemetrySourceName = TracingSourceName, + // Point the file memory at a local folder for persistent memory across sessions. + FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "agent-files")), + // Add the HyperlightCodeActProvider so the agent can execute Python code in a sandbox. + AIContextProviders = [codeAct], + ChatOptions = new ChatOptions + { + Instructions = instructions, + MaxOutputTokens = MaxOutputTokens, + Reasoning = new() { Effort = ReasoningEffort.Medium }, + }, + }); + +// Run the interactive console session using the shared HarnessConsole helper. +await HarnessConsole.RunAgentAsync( + agent, + userPrompt: "Ask me a technical question, or try: \"Help me write a regex that matches valid email addresses.\"", + new HarnessConsoleOptions + { + Observers = HarnessConsoleOptions.BuildObserversWithPlanning( + agent, + planModeName: "plan", + executionModeName: "execute", + maxContextWindowTokens: MaxContextWindowTokens, + maxOutputTokens: MaxOutputTokens), + CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent), + }); diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md new file mode 100644 index 0000000000..0d1b109bee --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/README.md @@ -0,0 +1,51 @@ +# Harness Step 04 — Code Execution (Hyperlight + Skills) + +This sample demonstrates a HarnessAgent with **all features enabled**, plus: + +- **Hyperlight CodeAct** — sandboxed Python code execution via `execute_code` (requires KVM) +- **Skills** — file-based skill discovery (a `regex-tester` skill is included) + +The agent can plan tasks, manage modes, store memories, read/write files, search the web, approve sensitive operations, discover and use skills, and execute arbitrary Python code — all pre-configured by the HarnessAgent. + +## Prerequisites + +- .NET 10 SDK +- An Azure AI Foundry project endpoint +- KVM-capable host (the Hyperlight sandbox runs code in micro-VMs) + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `AZURE_AI_PROJECT_ENDPOINT` | Your Azure AI Foundry project endpoint | +| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Model deployment name (default: `gpt-5.4`) | + +## Running + +```bash +dotnet run +``` + +## What to Try + +- **Regex testing**: "Help me write a regex that matches valid email addresses, then test it against some examples." +- **Code execution**: "Calculate the first 20 prime numbers using the Sieve of Eratosthenes." +- **Skill + code combo**: "I need a regex for ISO 8601 dates — test it thoroughly with edge cases." + +## Included Skill + +The `skills/regex-tester/` skill instructs the agent to validate regex patterns by executing Python test code in the Hyperlight sandbox. It includes a regex cheatsheet as reference material. + +## Features Enabled + +| Feature | Description | +|---------|-------------| +| TodoProvider | Task planning and tracking (`/todos` command) | +| AgentModeProvider | Mode switching (`/mode` command) | +| FileMemoryProvider | Persistent memory stored as files | +| FileAccessProvider | Read/write files in a working directory | +| ToolApproval | Don't-ask-again approval for sensitive tools | +| WebSearch | Built-in hosted web search | +| AgentSkillsProvider | Discovers and uses skills from the `skills/` folder | +| HyperlightCodeActProvider | Sandboxed Python execution via `execute_code` | +| OpenTelemetry | Trace logging to a text file | diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/SKILL.md b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/SKILL.md new file mode 100644 index 0000000000..7d1c9c49e3 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/SKILL.md @@ -0,0 +1,36 @@ +--- +name: regex-tester +description: Validate, test, and debug regular expressions by executing them against sample inputs. Use when asked to build, verify, or explain a regex pattern. +--- + +## Usage + +When the user asks you to create, validate, or debug a regular expression: + +1. **Understand the requirement** — clarify what the pattern should match and what it should reject. +2. **Consult the cheatsheet** — review `references/regex-cheatsheet.md` for syntax reminders if needed. +3. **Write and execute test code** — use the `execute_code` tool to run Python code that: + - Compiles the regex with `re.compile()` + - Tests it against a set of positive examples (should match) and negative examples (should not match) + - Extracts and displays any capturing groups + - Reports pass/fail for each test case +4. **Iterate** — if any test fails, refine the pattern and re-run until all cases pass. +5. **Present the result** — give the user the final pattern, explain what each part does, and show the test results. + +## Example Test Script + +```python +import re + +pattern = re.compile(r'^[\w.+-]+@[\w-]+\.[\w.-]+$') + +positives = ["user@example.com", "first.last+tag@sub.domain.org"] +negatives = ["@missing.com", "no-at-sign", "spaces in@address.com"] + +for s in positives: + assert pattern.match(s), f"FAIL: expected match for '{s}'" +for s in negatives: + assert not pattern.match(s), f"FAIL: expected no match for '{s}'" + +print("All tests passed!") +``` diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/references/regex-cheatsheet.md b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/references/regex-cheatsheet.md new file mode 100644 index 0000000000..342719673a --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/skills/regex-tester/references/regex-cheatsheet.md @@ -0,0 +1,97 @@ +# Regex Quick Reference (Python `re` module) + +## Character Classes + +| Pattern | Matches | +|---------|---------| +| `.` | Any character except newline | +| `\d` | Digit `[0-9]` | +| `\D` | Non-digit | +| `\w` | Word character `[a-zA-Z0-9_]` | +| `\W` | Non-word character | +| `\s` | Whitespace `[ \t\n\r\f\v]` | +| `\S` | Non-whitespace | +| `[abc]` | Any of a, b, or c | +| `[^abc]`| Any character except a, b, c | +| `[a-z]` | Range: a through z | + +## Quantifiers + +| Pattern | Meaning | +|---------|---------| +| `*` | 0 or more (greedy) | +| `+` | 1 or more (greedy) | +| `?` | 0 or 1 (greedy) | +| `{n}` | Exactly n | +| `{n,}` | n or more | +| `{n,m}` | Between n and m | +| `*?`, `+?`, `??` | Non-greedy versions | + +## Anchors + +| Pattern | Meaning | +|---------|---------| +| `^` | Start of string (or line with `re.MULTILINE`) | +| `$` | End of string (or line with `re.MULTILINE`) | +| `\b` | Word boundary | +| `\B` | Non-word boundary | + +## Groups and Backreferences + +| Pattern | Meaning | +|---------|---------| +| `(...)` | Capturing group | +| `(?:...)`| Non-capturing group | +| `(?P...)` | Named group | +| `\1` | Backreference to group 1 | +| `(?=...)` | Positive lookahead | +| `(?!...)` | Negative lookahead | +| `(?<=...)` | Positive lookbehind | +| `(?\d{4})-(?P\d{2})-(?P\d{2})', "2025-01-15") +m.group('year') # '2025' + +# Replace +re.sub(r'\d+', 'X', "abc 123 def") # 'abc X def' + +# Split +re.split(r',+', "a,b,,c") # ['a', 'b', 'c'] + +# Compile for reuse +pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$') +pattern.match("2025-01-15") # Match object +``` From 61f636ffb81a16acb98391c9a21721e39d5dce57 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 19 May 2026 20:10:57 +0100 Subject: [PATCH 07/22] .NET: Reduce re-rendering in harness console (#5953) * Reduce re-rendering in harness console * Address PR comments * Fix broken merge --- .../ListSelection.cs | 4 +- .../ConsoleReactiveComponents/TextInput.cs | 6 +- .../ConsoleReactiveComponents/TextPanel.cs | 10 +-- .../TextScrollPanel.cs | 2 +- .../TopBottomRule.cs | 16 ++-- .../ConsoleReactiveComponent.cs | 64 ++++++++++---- .../Components/AgentModeAndHelp.cs | 2 +- .../Components/AgentStatus.cs | 2 +- .../HarnessAppComponent.cs | 83 +++++++++++-------- 9 files changed, 116 insertions(+), 73 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/ListSelection.cs b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/ListSelection.cs index 8a0c8b982e..eedc76a145 100644 --- a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/ListSelection.cs +++ b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/ListSelection.cs @@ -39,7 +39,7 @@ public class ListSelection : ConsoleReactiveComponent /// A component that renders a list of pre-rendered string items vertically. /// Designed for rendering dynamic items in a non-scroll region that may be -/// re-rendered on each update. If the component's +/// re-rendered on each update. If the component's /// exceeds the number of output lines, leftover lines are erased. /// public class TextPanel : ConsoleReactiveComponent @@ -51,18 +51,18 @@ public class TextPanel : ConsoleReactiveComponent currentRow) + if (props.Height > currentRow) { - for (int i = currentRow; i < this.Height; i++) + for (int i = currentRow; i < props.Height; i++) { - Console.Write(AnsiEscapes.MoveAndEraseLine(this.Y + i)); + Console.Write(AnsiEscapes.MoveAndEraseLine(props.Y + i)); } } } diff --git a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextScrollPanel.cs b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextScrollPanel.cs index f0b156cd5a..15147b0fd0 100644 --- a/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextScrollPanel.cs +++ b/dotnet/samples/02-agents/Harness/ConsoleReactiveComponents/TextScrollPanel.cs @@ -52,7 +52,7 @@ public class TextScrollPanel : ConsoleReactiveComponent public record TopBottomRuleProps : ConsoleReactiveProps { - /// Gets the width of the horizontal rules in characters. - public int Width { get; init; } - /// Gets the foreground color of the horizontal rules. If null, the default terminal color is used. public ConsoleColor? Color { get; init; } } @@ -32,7 +29,7 @@ public class TopBottomRule : ConsoleReactiveComponent -/// Abstract base class for all console UI components. Provides layout properties -/// (position and size) and a method for drawing to the console. +/// Abstract base class for all console UI components. Provides access to layout +/// through and a method for drawing to the console. /// Derive from instead of this class directly. /// public abstract class ConsoleReactiveComponent @@ -13,20 +13,21 @@ public abstract class ConsoleReactiveComponent { } - /// Gets or sets the 1-based column position of the component. - public int X { get; set; } - - /// Gets or sets the 1-based row position of the component. - public int Y { get; set; } - - /// Gets or sets the width of the component in columns. - public int Width { get; set; } - - /// Gets or sets the height of the component in rows. - public int Height { get; set; } + /// + /// Gets or sets the component's props as the base type. + /// Used by parent components to set layout (X, Y, Width, Height) on children without + /// knowing the concrete props type. + /// + public abstract ConsoleReactiveProps? BaseProps { get; set; } /// Renders the component to the console at its current position. public abstract void Render(); + + /// + /// Invalidates the component's cached render state, causing the next call + /// to proceed even if props and state have not changed. Use after a screen erase to force repaint. + /// + public abstract void Invalidate(); } /// @@ -46,6 +47,13 @@ public abstract class ConsoleReactiveComponent : ConsoleReactive /// Gets or sets the component's props (external configuration). public TProps? Props { get; set; } + /// + public override ConsoleReactiveProps? BaseProps + { + get => this.Props; + set => this.Props = (TProps?)value; + } + /// Gets or sets the component's internal state. protected TState? State { get; set; } @@ -73,8 +81,8 @@ public abstract class ConsoleReactiveComponent : ConsoleReactive return; } - if (ReferenceEquals(this.Props, this._lastRenderedProps) - && ReferenceEquals(this.State, this._lastRenderedState)) + if (EqualityComparer.Default.Equals(this.Props, this._lastRenderedProps) + && EqualityComparer.Default.Equals(this.State, this._lastRenderedState)) { return; } @@ -86,6 +94,16 @@ public abstract class ConsoleReactiveComponent : ConsoleReactive } } + /// + public override void Invalidate() + { + lock (this._renderLock) + { + this._lastRenderedProps = default; + this._lastRenderedState = default; + } + } + /// /// Called by to perform the actual rendering. Override this in derived classes. /// @@ -95,11 +113,23 @@ public abstract class ConsoleReactiveComponent : ConsoleReactive } /// -/// Base record for component props. Provides an optional collection -/// for composing child components. +/// Base record for component props. Provides layout properties (position and size) +/// and an optional collection for composing child components. /// public record ConsoleReactiveProps { + /// Gets the 1-based column position of the component. + public int X { get; init; } + + /// Gets the 1-based row position of the component. + public int Y { get; init; } + + /// Gets the width of the component in columns. + public int Width { get; init; } + + /// Gets the height of the component in rows. + public int Height { get; init; } + /// Gets the child components to render within this component. public IReadOnlyList Children { get; init; } = []; } diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentModeAndHelp.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentModeAndHelp.cs index 97579992fd..2e1d86a413 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentModeAndHelp.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Components/AgentModeAndHelp.cs @@ -43,7 +43,7 @@ public class AgentModeAndHelp : ConsoleReactiveComponent Date: Tue, 19 May 2026 21:33:11 +0200 Subject: [PATCH 08/22] ci(python-setup): drop -U upgrade flag from uv sync (#5961) The shared composite action ran `uv sync --all-packages --all-extras --dev -U` on every job, which upgrades every dependency to the latest compatible version instead of using the pinned versions in `uv.lock`. That is currently producing a hard resolver failure on every CI job: No solution found when resolving dependencies for split (markers: python_full_version >= '3.11' and sys_platform == 'darwin') Because there are no versions of durabletask and agent-framework-durabletask depends on durabletask>=1.3.0,<2, we can conclude that agent-framework-durabletask's requirements are unsatisfiable. Dropping `-U` makes the install use the workspace lockfile, which is what is reproducible locally and what we publish releases against. Upgrades should be opt-in (via a scheduled job or a separate workflow) rather than implicit on every CI run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/python-setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/python-setup/action.yml b/.github/actions/python-setup/action.yml index e81180fc28..3f5be8e792 100644 --- a/.github/actions/python-setup/action.yml +++ b/.github/actions/python-setup/action.yml @@ -40,4 +40,4 @@ runs: - name: Install the project shell: bash run: | - cd python && uv sync --all-packages --all-extras --dev -U --prerelease=if-necessary-or-explicit + cd python && uv sync --all-packages --all-extras --dev --prerelease=if-necessary-or-explicit From 4b0522d62d9b4a5e3fc952ac55c7ecbc59aa6a11 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Wed, 20 May 2026 09:20:53 +0900 Subject: [PATCH 09/22] Python: Bump Python package versions for a release (#5964) * Bump Python package versions to 1.5.0 for a release * Promote orchestrations to 1.0.0rc1 * ci(python-setup): merge dynamic exclude into existing workspace exclude The python-setup action injected exclude = [...] verbatim into [tool.uv.workspace], producing a duplicate 'exclude' key when the section already had a static exclude. Scope the rewrite to the [tool.uv.workspace] section and append the package to the existing array when present; idempotent if the package is already excluded. * Address Copilot review feedback: raise inter-package floors to 1.5.0 - foundry, foundry-local: agent-framework-openai >=1.4.0 -> >=1.5.0 - azure-contentunderstanding: agent-framework-foundry >=1.4.0 -> >=1.5.0 - azurefunctions: pin agent-framework-durabletask to >=1.0.0b260519,<2 Keeps lockstep cohort consistent and avoids mixed 1.4.x / 1.5.0 installs. * Re-include azurefunctions and durabletask in the uv workspace The pinned durabletask>=1.4.0 floor is enough to make resolution succeed; the workspace exclude was over-correction and broke CI samples and pyright type-checking (re-exports in agent_framework/azure/__init__.pyi plus samples/04-hosting/{azure_functions,durabletask}/ could not resolve their imports). Dropping them from agent-framework-core[all] still stands so the metapackage does not pull them. * Restore azurefunctions and durabletask in agent-framework-core[all] The durabletask floor pin keeps users on the safe 1.4.0, so they are once again included in the metapackage. Update CHANGELOG to reflect the pin rather than an [all] removal. * Raise uvicorn ceiling in ag-ui and devui to allow 0.42+ The root override-dependencies pins uvicorn[standard]>=0.34.0 (no upper) and the workspace lock resolves to 0.47.0. The package ceiling <0.42.0 meant the workspace was no longer testing the declared supported range. Bump to <1 so the lock fits within the declared bounds. Also picked up by validate-dependency-bounds: refresh stale orchestrations RC pin in devui dev deps. --- .github/actions/python-setup/action.yml | 8 +- python/CHANGELOG.md | 20 +- python/packages/a2a/pyproject.toml | 4 +- python/packages/ag-ui/pyproject.toml | 4 +- python/packages/anthropic/pyproject.toml | 4 +- .../packages/azure-ai-search/pyproject.toml | 4 +- .../azure-contentunderstanding/pyproject.toml | 6 +- python/packages/azure-cosmos/pyproject.toml | 4 +- python/packages/azurefunctions/pyproject.toml | 6 +- python/packages/bedrock/pyproject.toml | 4 +- python/packages/chatkit/pyproject.toml | 4 +- python/packages/claude/pyproject.toml | 4 +- python/packages/copilotstudio/pyproject.toml | 4 +- python/packages/core/pyproject.toml | 2 +- python/packages/declarative/pyproject.toml | 4 +- python/packages/devui/pyproject.toml | 8 +- python/packages/durabletask/pyproject.toml | 8 +- python/packages/foundry/pyproject.toml | 6 +- .../packages/foundry_hosting/pyproject.toml | 4 +- python/packages/foundry_local/pyproject.toml | 6 +- python/packages/gemini/pyproject.toml | 4 +- python/packages/github_copilot/pyproject.toml | 4 +- python/packages/hyperlight/pyproject.toml | 4 +- python/packages/lab/pyproject.toml | 4 +- python/packages/mem0/pyproject.toml | 4 +- python/packages/ollama/pyproject.toml | 4 +- python/packages/openai/pyproject.toml | 4 +- python/packages/orchestrations/pyproject.toml | 4 +- python/packages/purview/pyproject.toml | 4 +- python/packages/redis/pyproject.toml | 4 +- python/pyproject.toml | 4 +- python/uv.lock | 2910 +++++++++-------- 32 files changed, 1604 insertions(+), 1464 deletions(-) diff --git a/.github/actions/python-setup/action.yml b/.github/actions/python-setup/action.yml index 3f5be8e792..ed595ee87a 100644 --- a/.github/actions/python-setup/action.yml +++ b/.github/actions/python-setup/action.yml @@ -32,7 +32,13 @@ runs: if grep -q "name = \"$pkg\"" "$f"; then pkg_dir=$(dirname "$f" | sed 's|python/||') echo "Excluding workspace package: $pkg ($pkg_dir)" - sed -i.bak '/\[tool\.uv\.workspace\]/a\exclude = ["'"$pkg_dir"'"]' python/pyproject.toml + if awk '/^\[tool\.uv\.workspace\]/{f=1;next} /^\[/{f=0} f && /^exclude = \[/{found=1} END{exit !found}' python/pyproject.toml; then + if ! awk '/^\[tool\.uv\.workspace\]/{f=1;next} /^\[/{f=0} f && /^exclude = \[/ && index($0, "\"'"$pkg_dir"'\"")' python/pyproject.toml | grep -q .; then + sed -i.bak '/\[tool\.uv\.workspace\]/,/^\[/ { /^exclude = \[/ s|\]|, "'"$pkg_dir"'"]| }' python/pyproject.toml + fi + else + sed -i.bak '/\[tool\.uv\.workspace\]/a\exclude = ["'"$pkg_dir"'"]' python/pyproject.toml + fi sed -i.bak '/'"$pkg"' = { workspace = true }/d' python/pyproject.toml fi done diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index e800652483..6fe6871b74 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.5.0] - 2026-05-19 + +### Added +- **agent-framework-core**, **agent-framework-foundry**, **agent-framework-openai**: Record actual served model from Azure OpenAI ([#5910](https://github.com/microsoft/agent-framework/pull/5910)) +- **samples**: New Foundry Hosted Agents samples for RAG, Skills, and Memory ([#5822](https://github.com/microsoft/agent-framework/pull/5822)) + +### Changed +- **agent-framework-core**, **agent-framework-azurefunctions**, **agent-framework-devui**, **agent-framework-foundry**, **agent-framework-orchestrations**: Improve handling of intermediate outputs for workflows and orchestrations ([#5623](https://github.com/microsoft/agent-framework/pull/5623)) +- **agent-framework-durabletask**: Pin `durabletask` and `durabletask-azuremanaged` floors to `>=1.4.0` and exclude upstream `durabletask` 1.4.1, 1.4.2, and 1.4.3 from the supported version range. +- **agent-framework-orchestrations**: Bumped package to release candidate stage. + +### Fixed +- **agent-framework-core**: Parse YAML block scalars in SKILL.md frontmatter ([#5863](https://github.com/microsoft/agent-framework/pull/5863)) +- **agent-framework-github-copilot**: Include tools added by `ContextProvider.before_run` in session creation ([#5780](https://github.com/microsoft/agent-framework/pull/5780)) +- **agent-framework-hyperlight**: Skip symlinks when staging sandbox input ([#5919](https://github.com/microsoft/agent-framework/pull/5919)) +- **agent-framework-purview**: Remove duplicate pop in `InMemoryCacheProvider.remove` ([#5795](https://github.com/microsoft/agent-framework/pull/5795)) + ## [1.4.0] - 2026-05-14 ### Added @@ -1071,7 +1088,8 @@ Release candidate for **agent-framework-core** and **agent-framework-azure-ai** For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/). -[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.4.0...HEAD +[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.5.0...HEAD +[1.5.0]: https://github.com/microsoft/agent-framework/compare/python-1.4.0...python-1.5.0 [1.4.0]: https://github.com/microsoft/agent-framework/compare/python-1.3.0...python-1.4.0 [1.3.0]: https://github.com/microsoft/agent-framework/compare/python-1.2.2...python-1.3.0 [1.2.2]: https://github.com/microsoft/agent-framework/compare/python-1.2.1...python-1.2.2 diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml index 43ce8e985f..061c8a0409 100644 --- a/python/packages/a2a/pyproject.toml +++ b/python/packages/a2a/pyproject.toml @@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "a2a-sdk>=1.0.0,<2", ] diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index cdd5bda432..c1785b93f1 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -22,10 +22,10 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "ag-ui-protocol>=0.1.16,<0.2", "fastapi>=0.115.0,<0.133.1", - "uvicorn[standard]>=0.30.0,<0.42.0" + "uvicorn[standard]>=0.30.0,<1" ] [project.optional-dependencies] diff --git a/python/packages/anthropic/pyproject.toml b/python/packages/anthropic/pyproject.toml index 619523bb4c..268e4ce3c8 100644 --- a/python/packages/anthropic/pyproject.toml +++ b/python/packages/anthropic/pyproject.toml @@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "anthropic>=0.80.0,<0.80.1", ] diff --git a/python/packages/azure-ai-search/pyproject.toml b/python/packages/azure-ai-search/pyproject.toml index 1e0416a671..df89b6fe9d 100644 --- a/python/packages/azure-ai-search/pyproject.toml +++ b/python/packages/azure-ai-search/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "azure-search-documents>=11.7.0b2,<11.7.0b3", ] diff --git a/python/packages/azure-contentunderstanding/pyproject.toml b/python/packages/azure-contentunderstanding/pyproject.toml index 2fc4f35c50..b79fff655e 100644 --- a/python/packages/azure-contentunderstanding/pyproject.toml +++ b/python/packages/azure-contentunderstanding/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure Content Understanding integration for Microsoft Agent Frame authors = [{ name = "Microsoft", email = "af-support@microsoft.com" }] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0a260514" +version = "1.0.0a260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,8 +23,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", - "agent-framework-foundry>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", + "agent-framework-foundry>=1.5.0,<2", "azure-ai-contentunderstanding>=1.0.1,<1.1", "aiohttp>=3.9,<4", "filetype>=1.2,<2", diff --git a/python/packages/azure-cosmos/pyproject.toml b/python/packages/azure-cosmos/pyproject.toml index c65401c3b7..39ccf3741c 100644 --- a/python/packages/azure-cosmos/pyproject.toml +++ b/python/packages/azure-cosmos/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure Cosmos DB history provider integration for Microsoft Agent authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "azure-cosmos>=4.3.0,<5", ] diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index e779d7276c..0bf5fe5201 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,8 +22,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", - "agent-framework-durabletask", + "agent-framework-core>=1.5.0,<2", + "agent-framework-durabletask>=1.0.0b260519,<2", "azure-functions>=1.24.0,<2", "azure-functions-durable>=1.3.1,<2", ] diff --git a/python/packages/bedrock/pyproject.toml b/python/packages/bedrock/pyproject.toml index 281c9009ef..8ad36393ab 100644 --- a/python/packages/bedrock/pyproject.toml +++ b/python/packages/bedrock/pyproject.toml @@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "boto3>=1.35.0,<2.0.0", "botocore>=1.35.0,<2.0.0", ] diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml index b894d63245..2a3909599f 100644 --- a/python/packages/chatkit/pyproject.toml +++ b/python/packages/chatkit/pyproject.toml @@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "openai-chatkit>=1.4.1,<2.0.0", ] diff --git a/python/packages/claude/pyproject.toml b/python/packages/claude/pyproject.toml index f35c92187f..cc78edbb2b 100644 --- a/python/packages/claude/pyproject.toml +++ b/python/packages/claude/pyproject.toml @@ -4,7 +4,7 @@ description = "Claude Agent SDK integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "claude-agent-sdk>=0.1.36,<0.1.49", ] diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml index 121364c18f..6fc1954cac 100644 --- a/python/packages/copilotstudio/pyproject.toml +++ b/python/packages/copilotstudio/pyproject.toml @@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2", ] diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index d90dbc1db2..09e189fa95 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.4.0" +version = "1.5.0" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index 419457a3a1..ae745693f1 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "httpx>=0.27,<1", "powerfx>=0.0.32,<0.0.35; python_version < '3.14'", "pyyaml>=6.0,<7.0", diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml index e32eb5f486..7fa735e6a0 100644 --- a/python/packages/devui/pyproject.toml +++ b/python/packages/devui/pyproject.toml @@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://github.com/microsoft/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,18 +23,18 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "openai>=1.99.0,<3", "opentelemetry-sdk>=1.39.0,<2", "fastapi>=0.115.0,<0.133.1", - "uvicorn[standard]>=0.30.0,<0.42.0" + "uvicorn[standard]>=0.30.0,<1" ] [project.optional-dependencies] dev = [ "pytest==9.0.3", "watchdog==6.0.0", - "agent-framework-orchestrations==1.0.0b260402", + "agent-framework-orchestrations==1.0.0rc1", ] all = [ "pytest==9.0.3", diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index 2d737c9a75..27008b5a79 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,9 +22,9 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", - "durabletask>=1.3.0,<2", - "durabletask-azuremanaged>=1.3.0,<2", + "agent-framework-core>=1.5.0,<2", + "durabletask>=1.4.0,!=1.4.1,!=1.4.2,!=1.4.3,<2", + "durabletask-azuremanaged>=1.4.0,<2", "python-dateutil>=2.8.0,<3", ] diff --git a/python/packages/foundry/pyproject.toml b/python/packages/foundry/pyproject.toml index 405b5f7559..5bb464011d 100644 --- a/python/packages/foundry/pyproject.toml +++ b/python/packages/foundry/pyproject.toml @@ -4,7 +4,7 @@ description = "Microsoft Foundry integrations for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.4.0" +version = "1.5.0" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,8 +23,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", - "agent-framework-openai>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", + "agent-framework-openai>=1.5.0,<2", "azure-ai-inference>=1.0.0b9,<1.0.0b10", "azure-ai-projects>=2.1.0,<3.0", ] diff --git a/python/packages/foundry_hosting/pyproject.toml b/python/packages/foundry_hosting/pyproject.toml index 35a218a6c1..b924ee4ef6 100644 --- a/python/packages/foundry_hosting/pyproject.toml +++ b/python/packages/foundry_hosting/pyproject.toml @@ -4,7 +4,7 @@ description = "Foundry Hosting integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0a260514" +version = "1.0.0a260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "azure-ai-agentserver-core>=2.0.0b3,<3", "azure-ai-agentserver-responses>=1.0.0b5,<2", "azure-ai-agentserver-invocations>=1.0.0b3,<2", diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 88085ca1b0..6e45c50440 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,8 +23,8 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", - "agent-framework-openai>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", + "agent-framework-openai>=1.5.0,<2", "foundry-local-sdk>=0.5.1,<0.5.2", ] diff --git a/python/packages/gemini/pyproject.toml b/python/packages/gemini/pyproject.toml index dbeb4b261c..751112a4c1 100644 --- a/python/packages/gemini/pyproject.toml +++ b/python/packages/gemini/pyproject.toml @@ -4,7 +4,7 @@ description = "Google Gemini integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0a260514" +version = "1.0.0a260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -24,7 +24,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2.0", + "agent-framework-core>=1.5.0,<2.0", "google-genai>=1.65.0,<2.0.0", ] diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 85dae1802b..c46071db4c 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'", ] diff --git a/python/packages/hyperlight/pyproject.toml b/python/packages/hyperlight/pyproject.toml index a7895c3128..99514261eb 100644 --- a/python/packages/hyperlight/pyproject.toml +++ b/python/packages/hyperlight/pyproject.toml @@ -4,7 +4,7 @@ description = "Hyperlight CodeAct integrations for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "hyperlight-sandbox>=0.4.0,<0.5", "hyperlight-sandbox-backend-wasm>=0.4.0,<0.5 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'", "hyperlight-sandbox-python-guest>=0.4.0,<0.5", diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml index 21aa8f82a2..43048a73ec 100644 --- a/python/packages/lab/pyproject.toml +++ b/python/packages/lab/pyproject.toml @@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework" authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -22,7 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", ] [project.optional-dependencies] diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index 16eb3ca1d9..863342341d 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "mem0ai>=1.0.0,<2", ] diff --git a/python/packages/ollama/pyproject.toml b/python/packages/ollama/pyproject.toml index b785326d74..67d9d5a54e 100644 --- a/python/packages/ollama/pyproject.toml +++ b/python/packages/ollama/pyproject.toml @@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "ollama>=0.5.3,<0.5.4", ] diff --git a/python/packages/openai/pyproject.toml b/python/packages/openai/pyproject.toml index 7db5a6ec36..a6e94cf555 100644 --- a/python/packages/openai/pyproject.toml +++ b/python/packages/openai/pyproject.toml @@ -4,7 +4,7 @@ description = "OpenAI integrations for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.4.0" +version = "1.5.0" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "openai>=1.99.0,<3", ] diff --git a/python/packages/orchestrations/pyproject.toml b/python/packages/orchestrations/pyproject.toml index 7325a3e59a..843725cbe3 100644 --- a/python/packages/orchestrations/pyproject.toml +++ b/python/packages/orchestrations/pyproject.toml @@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0rc1" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", ] [tool.uv] diff --git a/python/packages/purview/pyproject.toml b/python/packages/purview/pyproject.toml index 777fc49d31..5ac36f1bad 100644 --- a/python/packages/purview/pyproject.toml +++ b/python/packages/purview/pyproject.toml @@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://github.com/microsoft/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -24,7 +24,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "azure-core>=1.30.0,<2", "httpx>=0.27.0,<0.29", ] diff --git a/python/packages/redis/pyproject.toml b/python/packages/redis/pyproject.toml index df2ef9a518..13a8ac8ed2 100644 --- a/python/packages/redis/pyproject.toml +++ b/python/packages/redis/pyproject.toml @@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework." authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.0.0b260514" +version = "1.0.0b260519" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core>=1.4.0,<2", + "agent-framework-core>=1.5.0,<2", "redis>=6.4.0,<7.2.1", "redisvl>=0.11.0,<0.16", "numpy>=2.2.6,<3" diff --git a/python/pyproject.toml b/python/pyproject.toml index d7aa3d3496..c1ad252cba 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] readme = "README.md" requires-python = ">=3.10" -version = "1.4.0" +version = "1.5.0" license-files = ["LICENSE"] urls.homepage = "https://aka.ms/agent-framework" urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" @@ -23,7 +23,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "agent-framework-core[all]==1.4.0", + "agent-framework-core[all]==1.5.0", ] [dependency-groups] diff --git a/python/uv.lock b/python/uv.lock index 5154479024..9051f077fc 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -67,7 +67,7 @@ overrides = [ [[package]] name = "a2a-sdk" -version = "1.0.2" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "culsans", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, @@ -80,9 +80,9 @@ dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/f3/1c312eae0298542eef1a096be378a3ad2d20b171ea0ac6be26b81f542720/a2a_sdk-1.0.2.tar.gz", hash = "sha256:e4ee4dd509894c32c9a6df728319875fa4f049e70ae82476fa447353e3a4b648", size = 375193, upload-time = "2026-04-24T13:50:24.303Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/35/8b7ac94f405f57c591925fa0afc105a0f797151876fffa666b57722eefa9/a2a_sdk-1.0.3.tar.gz", hash = "sha256:c57ddd910aece4a426ae26b8f0d0e8e2f3271a6adde974078075e4f600aaf628", size = 367155, upload-time = "2026-05-13T06:52:33.929Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/03/58c92a44e7b94a42614880df2365f074969e47067c4c736e31e855aca2fd/a2a_sdk-1.0.2-py3-none-any.whl", hash = "sha256:4dbc083b6808ee28207ac6daad263360f87612c37b2d06f5521efb530318141c", size = 234302, upload-time = "2026-04-24T13:50:22.412Z" }, + { url = "https://files.pythonhosted.org/packages/53/6f/ae79f8210f1ecd70e1c37c310a523b26f1d6da458d4c1365914bf1ea58e0/a2a_sdk-1.0.3-py3-none-any.whl", hash = "sha256:068e5b2ceb4e962ac61d9e1fd43ca0c1016b64f0c80d901f6e23420bc8a31a93", size = 235705, upload-time = "2026-05-13T06:52:31.88Z" }, ] [[package]] @@ -108,7 +108,7 @@ wheels = [ [[package]] name = "agent-framework" -version = "1.4.0" +version = "1.5.0" source = { virtual = "." } dependencies = [ { name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -163,7 +163,7 @@ dev = [ [[package]] name = "agent-framework-a2a" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/a2a" } dependencies = [ { name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -200,13 +200,13 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115.0,<0.133.1" }, { name = "httpx", marker = "extra == 'dev'", specifier = "==0.28.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==9.0.3" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0,<0.42.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0,<1" }, ] provides-extras = ["dev"] [[package]] name = "agent-framework-anthropic" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/anthropic" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -221,7 +221,7 @@ requires-dist = [ [[package]] name = "agent-framework-azure-ai-search" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/azure-ai-search" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -236,7 +236,7 @@ requires-dist = [ [[package]] name = "agent-framework-azure-contentunderstanding" -version = "1.0.0a260514" +version = "1.0.0a260519" source = { editable = "packages/azure-contentunderstanding" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -257,7 +257,7 @@ requires-dist = [ [[package]] name = "agent-framework-azure-cosmos" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/azure-cosmos" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -272,7 +272,7 @@ requires-dist = [ [[package]] name = "agent-framework-azurefunctions" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/azurefunctions" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -294,7 +294,7 @@ dev = [] [[package]] name = "agent-framework-bedrock" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/bedrock" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -311,7 +311,7 @@ requires-dist = [ [[package]] name = "agent-framework-chatkit" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/chatkit" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -326,7 +326,7 @@ requires-dist = [ [[package]] name = "agent-framework-claude" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/claude" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -341,7 +341,7 @@ requires-dist = [ [[package]] name = "agent-framework-copilotstudio" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/copilotstudio" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -356,7 +356,7 @@ requires-dist = [ [[package]] name = "agent-framework-core" -version = "1.4.0" +version = "1.5.0" source = { editable = "packages/core" } dependencies = [ { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -430,7 +430,7 @@ provides-extras = ["all"] [[package]] name = "agent-framework-declarative" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/declarative" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -457,7 +457,7 @@ dev = [{ name = "types-pyyaml", specifier = "==6.0.12.20250915" }] [[package]] name = "agent-framework-devui" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/devui" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -487,7 +487,7 @@ requires-dist = [ { name = "opentelemetry-sdk", specifier = ">=1.39.0,<2" }, { name = "pytest", marker = "extra == 'all'", specifier = "==9.0.3" }, { name = "pytest", marker = "extra == 'dev'", specifier = "==9.0.3" }, - { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0,<0.42.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0,<1" }, { name = "watchdog", marker = "extra == 'all'", specifier = "==6.0.0" }, { name = "watchdog", marker = "extra == 'dev'", specifier = "==6.0.0" }, ] @@ -495,7 +495,7 @@ provides-extras = ["dev", "all"] [[package]] name = "agent-framework-durabletask" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/durabletask" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -512,8 +512,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "durabletask", specifier = ">=1.3.0,<2" }, - { name = "durabletask-azuremanaged", specifier = ">=1.3.0,<2" }, + { name = "durabletask", specifier = ">=1.4.0,!=1.4.1,!=1.4.2,!=1.4.3,<2" }, + { name = "durabletask-azuremanaged", specifier = ">=1.4.0,<2" }, { name = "python-dateutil", specifier = ">=2.8.0,<3" }, ] @@ -522,7 +522,7 @@ dev = [{ name = "types-python-dateutil", specifier = "==2.9.0.20260402" }] [[package]] name = "agent-framework-foundry" -version = "1.4.0" +version = "1.5.0" source = { editable = "packages/foundry" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -541,7 +541,7 @@ requires-dist = [ [[package]] name = "agent-framework-foundry-hosting" -version = "1.0.0a260514" +version = "1.0.0a260519" source = { editable = "packages/foundry_hosting" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -560,7 +560,7 @@ requires-dist = [ [[package]] name = "agent-framework-foundry-local" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/foundry_local" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -577,7 +577,7 @@ requires-dist = [ [[package]] name = "agent-framework-gemini" -version = "1.0.0a260514" +version = "1.0.0a260519" source = { editable = "packages/gemini" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -592,7 +592,7 @@ requires-dist = [ [[package]] name = "agent-framework-github-copilot" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/github_copilot" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -602,12 +602,12 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0b2,<=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, ] [[package]] name = "agent-framework-hyperlight" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/hyperlight" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -626,7 +626,7 @@ requires-dist = [ [[package]] name = "agent-framework-lab" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/lab" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -651,7 +651,7 @@ math = [ tau2 = [ { name = "loguru", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -707,7 +707,7 @@ dev = [ [[package]] name = "agent-framework-mem0" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/mem0" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -722,7 +722,7 @@ requires-dist = [ [[package]] name = "agent-framework-ollama" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/ollama" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -737,7 +737,7 @@ requires-dist = [ [[package]] name = "agent-framework-openai" -version = "1.4.0" +version = "1.5.0" source = { editable = "packages/openai" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -752,7 +752,7 @@ requires-dist = [ [[package]] name = "agent-framework-orchestrations" -version = "1.0.0b260514" +version = "1.0.0rc1" source = { editable = "packages/orchestrations" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -763,7 +763,7 @@ requires-dist = [{ name = "agent-framework-core", editable = "packages/core" }] [[package]] name = "agent-framework-purview" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/purview" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -780,12 +780,12 @@ requires-dist = [ [[package]] name = "agent-framework-redis" -version = "1.0.0b260514" +version = "1.0.0b260519" source = { editable = "packages/redis" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "redisvl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -861,7 +861,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.4" +version = "3.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -873,110 +873,110 @@ dependencies = [ { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "yarl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } +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/2c/05/6817e0390eb47b0867cf8efdb535298191662192281bc3ca62a0cb7973eb/aiohttp-3.13.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6290fe12fe8cefa6ea3c1c5b969d32c010dfe191d4392ff9b599a3f473cbe722", size = 753094, upload-time = "2026-03-28T17:14:59.928Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/e5b7f25f6dd1ab57da92aa9d226b2c8b56f223dd20475d3ddfddaba86ab8/aiohttp-3.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7520d92c0e8fbbe63f36f20a5762db349ff574ad38ad7bc7732558a650439845", size = 505213, upload-time = "2026-03-28T17:15:01.989Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e5/8f42033c7ce98b54dfd3791f03e60231cfe4a2db4471b5fc188df2b8a6ad/aiohttp-3.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2710ae1e1b81d0f187883b6e9d66cecf8794b50e91aa1e73fc78bfb5503b5d9", size = 498580, upload-time = "2026-03-28T17:15:03.879Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/bbc989f5362066b81930da1a66084a859a971d03faab799dc59a3ce3a220/aiohttp-3.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:717d17347567ded1e273aa09918650dfd6fd06f461549204570c7973537d4123", size = 1692718, upload-time = "2026-03-28T17:15:05.541Z" }, - { url = "https://files.pythonhosted.org/packages/1c/72/3775116969931f151be116689d2ae6ddafff2ec2887d8f9b4e7043f32e74/aiohttp-3.13.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:383880f7b8de5ac208fa829c7038d08e66377283b2de9e791b71e06e803153c2", size = 1660714, upload-time = "2026-03-28T17:15:08.23Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e8/d2f1a2da2743e32fe348ebf8a4c59caad14a92f5f18af616fd33381275e1/aiohttp-3.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1867087e2c1963db1216aedf001efe3b129835ed2b05d97d058176a6d08b5726", size = 1744152, upload-time = "2026-03-28T17:15:10.828Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a6/575886f417ac3c08e462f2ca237cc49f436bd992ca3f7ff95b7dd9c44205/aiohttp-3.13.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6234bf416a38d687c3ab7f79934d7fb2a42117a5b9813aca07de0a5398489023", size = 1836278, upload-time = "2026-03-28T17:15:12.537Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4c/0051d4550fb9e8b5ca4e0fe1ccd58652340915180c5164999e6741bf2083/aiohttp-3.13.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdd3393130bf6588962441ffd5bde1d3ea2d63a64afa7119b3f3ba349cebbe7", size = 1687953, upload-time = "2026-03-28T17:15:14.248Z" }, - { url = "https://files.pythonhosted.org/packages/c9/54/841e87b8c51c2adc01a3ceb9919dc45c7899fe4c21deb70aada734ea5a38/aiohttp-3.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d0dbc6c76befa76865373d6aa303e480bb8c3486e7763530f7f6e527b471118", size = 1572484, upload-time = "2026-03-28T17:15:15.911Z" }, - { url = "https://files.pythonhosted.org/packages/da/f1/21cbf5f7fa1e267af6301f886cab9b314f085e4d0097668d189d165cd7da/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10fb7b53262cf4144a083c9db0d2b4d22823d6708270a9970c4627b248c6064c", size = 1662851, upload-time = "2026-03-28T17:15:17.822Z" }, - { url = "https://files.pythonhosted.org/packages/40/15/bcad6b68d7bef27ae7443288215767263c7753ede164267cf6cf63c94a87/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:eb10ce8c03850e77f4d9518961c227be569e12f71525a7e90d17bca04299921d", size = 1671984, upload-time = "2026-03-28T17:15:19.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/fa/ab316931afc7a73c7f493bb1b30fbd61e28ec2d3ea50353336e76293e8ec/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7c65738ac5ae32b8feef699a4ed0dc91a0c8618b347781b7461458bbcaaac7eb", size = 1713880, upload-time = "2026-03-28T17:15:21.589Z" }, - { url = "https://files.pythonhosted.org/packages/1c/45/314e8e64c7f328174964b6db511dd5e9e60c9121ab5457bc2c908b7d03a4/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6b335919ffbaf98df8ff3c74f7a6decb8775882632952fd1810a017e38f15aee", size = 1560315, upload-time = "2026-03-28T17:15:23.66Z" }, - { url = "https://files.pythonhosted.org/packages/18/e7/93d5fa06fe00219a81466577dacae9e3732f3b4f767b12b2e2cc8c35c970/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ec75fc18cb9f4aca51c2cbace20cf6716e36850f44189644d2d69a875d5e0532", size = 1735115, upload-time = "2026-03-28T17:15:25.77Z" }, - { url = "https://files.pythonhosted.org/packages/19/9f/f64b95392ddd4e204fd9ab7cd33dd18d14ac9e4b86866f1f6a69b7cda83d/aiohttp-3.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:463fa18a95c5a635d2b8c09babe240f9d7dbf2a2010a6c0b35d8c4dff2a0e819", size = 1673916, upload-time = "2026-03-28T17:15:27.526Z" }, - { url = "https://files.pythonhosted.org/packages/52/c1/bb33be79fd285c69f32e5b074b299cae8847f748950149c3965c1b3b3adf/aiohttp-3.13.4-cp310-cp310-win32.whl", hash = "sha256:13168f5645d9045522c6cef818f54295376257ed8d02513a37c2ef3046fc7a97", size = 440277, upload-time = "2026-03-28T17:15:29.173Z" }, - { url = "https://files.pythonhosted.org/packages/23/f9/7cf1688da4dd0885f914ee40bc8e1dce776df98fe6518766de975a570538/aiohttp-3.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:a7058af1f53209fdf07745579ced525d38d481650a989b7aa4a3b484b901cdab", size = 463015, upload-time = "2026-03-28T17:15:30.802Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" }, - { url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" }, - { url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" }, - { url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" }, - { url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" }, - { url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" }, - { url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" }, - { url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" }, - { url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" }, - { url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" }, - { url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" }, - { url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" }, - { url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" }, - { url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" }, - { url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" }, - { url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" }, - { url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, - { url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, - { url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, - { url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, - { url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, - { url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, - { url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, - { url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, - { url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, - { url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, - { url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, - { url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, - { url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" }, - { url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" }, - { url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" }, - { url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" }, - { url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" }, - { url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" }, - { url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" }, - { url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" }, - { url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" }, - { url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" }, - { url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" }, - { url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" }, - { url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" }, - { url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" }, - { url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" }, - { url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" }, - { url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" }, - { url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" }, - { url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" }, - { url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" }, - { url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" }, - { url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" }, - { url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] [[package]] @@ -1217,28 +1217,28 @@ wheels = [ [[package]] name = "azure-core" -version = "1.39.0" +version = "1.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, ] [[package]] name = "azure-core-tracing-opentelemetry" -version = "1.0.0b12" +version = "1.0.0b13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/7f/5de13a331a5f2919417819cc37dcf7c897018f02f83aa82b733e6629a6a6/azure_core_tracing_opentelemetry-1.0.0b12.tar.gz", hash = "sha256:bb454142440bae11fd9d68c7c1d67ae38a1756ce808c5e4d736730a7b4b04144", size = 26010, upload-time = "2025-03-21T00:18:37.346Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/ab/a937e4af8afec9d437d55252f2a3a4419fc3fc7d5e5d54022622bd11b2b6/azure_core_tracing_opentelemetry-1.0.0b13.tar.gz", hash = "sha256:6cb2f8dfd5dee6c11843db0205fc92e2434e1a272c169c953afe92483aafc7eb", size = 25832, upload-time = "2026-05-01T00:59:57.941Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/5e/97a471f66935e7f89f521d0e11ae49c7f0871ca38f5c319dccae2155c8d8/azure_core_tracing_opentelemetry-1.0.0b12-py3-none-any.whl", hash = "sha256:38fd42709f1cc4bbc4f2797008b1c30a6a01617e49910c05daa3a0d0c65053ac", size = 11962, upload-time = "2025-03-21T00:18:38.581Z" }, + { url = "https://files.pythonhosted.org/packages/43/01/8898c2506cae6a57c1b76d930d2af94764a65354bc863feb2684235851ce/azure_core_tracing_opentelemetry-1.0.0b13-py3-none-any.whl", hash = "sha256:4dacd3a9f117f11f98e89305e161c951b8df85b984f3b56130614de9cd9887f9", size = 12112, upload-time = "2026-05-01T00:59:59.149Z" }, ] [[package]] @@ -1326,7 +1326,7 @@ wheels = [ [[package]] name = "azure-monitor-opentelemetry-exporter" -version = "1.0.0b51" +version = "1.0.0b52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -1336,9 +1336,9 @@ dependencies = [ { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/a4/a6cd2d389bc1009300bcd57c9e2ace4b7e7ae1e5dc0bda415ee803629cf2/azure_monitor_opentelemetry_exporter-1.0.0b51.tar.gz", hash = "sha256:a6171c34326bcd6216938bb40d715c15f1f22984ac1986fc97231336d8ac4c3c", size = 319837, upload-time = "2026-04-06T21:45:46.378Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/7e/bfc03436b88c48f5adc21a3ebbf4392b6b7fbbfe33ef3b1e88d07ba9f380/azure_monitor_opentelemetry_exporter-1.0.0b52.tar.gz", hash = "sha256:7eac679fca32dee9e426df65f2a538161db4514fc322fc66107f7826567d86e1", size = 326179, upload-time = "2026-05-11T22:47:02.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/1a/6b0b7a6181b42709103a65a676c89fd5055cb1d1b281ebe10c49254a170f/azure_monitor_opentelemetry_exporter-1.0.0b51-py2.py3-none-any.whl", hash = "sha256:6572cac11f96e3b18ae1187cb35cf3b40d0004655dae8048896c41c765bea530", size = 242104, upload-time = "2026-04-06T21:45:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e8/d13e6a74c98ecc3011bce9ab09fc2e75aec48ab46288f72be57c2fa21460/azure_monitor_opentelemetry_exporter-1.0.0b52-py2.py3-none-any.whl", hash = "sha256:a38c503e5e2cc0ec8a4bf336b23cce23488719f5361a45cdd01a514080f0e7fc", size = 244751, upload-time = "2026-05-11T22:47:04.304Z" }, ] [[package]] @@ -1400,30 +1400,149 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.59" +version = "1.43.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "s3transfer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/4e/499cb52aaee9468c346bcc1158965e24e72b4e2a20052725b680e0ac949b/boto3-1.42.59.tar.gz", hash = "sha256:6c4a14a4eb37b58a9048901bdeefbe1c529638b73e8f55413319a25f010ca211", size = 112725, upload-time = "2026-02-27T20:25:33.228Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/36/028c12ed6ed85009a21b5472eb76c27f9b0341c6986f06f83475b40aaf51/boto3-1.43.1.tar.gz", hash = "sha256:9e4f85a7884797ff0f52c257094730ed228aaa07fa8134775ff8f86909cf4f2a", size = 113175, upload-time = "2026-04-30T20:27:04.569Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/c0/22d868b9408dc5a33935a72896ec8d638b2766c459668d1b37c3e5ac2066/boto3-1.42.59-py3-none-any.whl", hash = "sha256:7a66e3e8e2087ea4403e135e9de592e6d63fc9a91080d8dac415bb74df873a72", size = 140557, upload-time = "2026-02-27T20:25:31.774Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d1/b8b2d5420c51cd8f7ec044ceecbf24b060156680b26519e1d482e160c3c8/boto3-1.43.1-py3-none-any.whl", hash = "sha256:3840bf0345b9aefcc5915176a19d227f63cfba7778c65e6e52d61c6ea0a10fdc", size = 140498, upload-time = "2026-04-30T20:27:01.791Z" }, ] [[package]] name = "botocore" -version = "1.42.97" +version = "1.43.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/95/c37edb602948fad2253ffd1bb3dba5b938645bd1845ee4160350136a0f41/botocore-1.42.97.tar.gz", hash = "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", size = 15269348, upload-time = "2026-04-27T20:39:05.625Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/fa/4bec16fa5a4cde7b593e549238bfeb8ed1bdba9d427888a18c460a1f2352/botocore-1.43.11.tar.gz", hash = "sha256:d7d479cc2809ec2728f2898521003adfb79bfe6a4615c59dfd222ec52b0cee6b", size = 15364020, upload-time = "2026-05-19T19:39:58.317Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/d2/8e025ba1a4e257879af72d06913272311af79673d82fa2581a351b924317/botocore-1.42.97-py3-none-any.whl", hash = "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc", size = 14950367, upload-time = "2026-04-27T20:39:01.261Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/9f1d955c2eebefb6bd20de740ae7a05e7b015c63f0f01dba338dcf29cc68/botocore-1.43.11-py3-none-any.whl", hash = "sha256:0108b5604df5a26918936c845e1e761866ee9ea8d1c1f9358ed3c69afdc37436", size = 15043467, upload-time = "2026-05-19T19:39:53.176Z" }, +] + +[[package]] +name = "cachebox" +version = "5.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/f6/85f176d2518cf1d1be5f981fc2dadf6b131e33fefd721f36b330e3434d6c/cachebox-5.2.3.tar.gz", hash = "sha256:b1f68246685aa739bbbd2734befb1465363a1e1042407c154feadb065f17a099", size = 63686, upload-time = "2026-04-10T12:21:35.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/9e/88193fcb7a2a43fe8ed9d9888374d43fa5c7176aa802651e68b28f1aee4a/cachebox-5.2.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c2c89720547271d36e10cad2c7302bbe11f46eb39eead0a2c321c2d371b8f8b6", size = 374393, upload-time = "2026-04-10T12:20:20.424Z" }, + { url = "https://files.pythonhosted.org/packages/98/8d/e0b13d9bfd43f295cce7824ebaac1970f818a7027c16f290de404934cafe/cachebox-5.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7f33d24e90dc8aa26762e25898c91a1223b66685420a28a3628fa2e006924f5", size = 356318, upload-time = "2026-04-10T12:20:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/bc/02/8ae1b63dbdebb2ebf600523f48b54e9bfb10db5a28551c3432346f49e1dd/cachebox-5.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cb03ec6289a2ac5daf7422d755683324f02d821bfa796087100df2a7ebd5de", size = 395782, upload-time = "2026-04-10T12:18:50.054Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2f/79a8a0057f354581c25a1a00ddabbd5db4b8631d192670d7a0cc4271dbb7/cachebox-5.2.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71a71df463ba4c86bc843fa01c3a2a721033adefad888af28c6b65e1915a75c", size = 353194, upload-time = "2026-04-10T12:19:03.083Z" }, + { url = "https://files.pythonhosted.org/packages/3b/57/a1fead35cf481432bd87def0653cd4a069b1ea5847589255795e49ae74b8/cachebox-5.2.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbe4655371d19fc9f4f5874312bcb6e5b5b6182989979ac33d93c34c8d10c012", size = 371090, upload-time = "2026-04-10T12:19:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/8c/58/53f1fab8bcc3238fd6c533ef3ab146097986a8acb722863c688a2410c1b2/cachebox-5.2.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4974476d1779961df89d6e6f79e6103a1659289d3ee11c92adcb52e236a8aaeb", size = 390902, upload-time = "2026-04-10T12:19:28.258Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/5abff74666f8388d2c9516c265f99c33484c827f7fcb3cd703c2f3cbb17e/cachebox-5.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad16d733219f4cab3eec6533af30ab7b9c919c6e3e22ad1ef4eb82629a62edef", size = 395855, upload-time = "2026-04-10T12:19:54.207Z" }, + { url = "https://files.pythonhosted.org/packages/dd/11/30b429db12ab5df663aa108bcfac42805f733da65b0bf452f60bfaf4a530/cachebox-5.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:12a9e0a93774ca2b3a9fe8a2a0d0812e399fac4af0fce6246a5bca1e7009b8fc", size = 425760, upload-time = "2026-04-10T12:19:41.138Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/fdac1bb902b954c03d23eb301d645a328c9664caff5898930fdbd92fde80/cachebox-5.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:be89497a011eb7a638d13cc520244d77579c0f515b95bf759b3de0b90a015203", size = 564988, upload-time = "2026-04-10T12:20:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/76cd5405b0339f15bf86593258bf9bc5608f10a5e0fa6f37a282b42a6caa/cachebox-5.2.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:dd01fc0c1934cccb76493eb4b149a9232d299e5e0275f557adf875c3d25cec81", size = 669110, upload-time = "2026-04-10T12:20:49.039Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bc/52d154aa0407bafce94d1d8d3ff27ca5e842f8311be43cfabdefcbb0f6b7/cachebox-5.2.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a0dfd97b0968f8bd48c33098a03d10f797964559c3a437c84bf97a9973545714", size = 643768, upload-time = "2026-04-10T12:21:04.095Z" }, + { url = "https://files.pythonhosted.org/packages/51/d9/82627eb8cecaf5e7e601bbc65d474a1c3053a2fbc21618ddc6aac19c47dc/cachebox-5.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:223ccf7ac60f595def258e7bc74c0b1d6f43991c9cae6d06749c803d22786d99", size = 610047, upload-time = "2026-04-10T12:21:19.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2e/cc5b303746418fde00c93ddbc295733b4e2d131d2e8f5afbc6f45f50454e/cachebox-5.2.3-cp310-cp310-win32.whl", hash = "sha256:745b805fdd99931c3ce1d87d2ee21ca3fb62cba6b4e1f674907af87aad73dce4", size = 275529, upload-time = "2026-04-10T12:21:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/31/72/fb10d6f779d041f701b89f0b7830329f51d1846fbc600869f9f7d635b7b5/cachebox-5.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:a87b19c0a3d8d665a9805b5b4afd64b40082395b70ebe2756131ed1edb0c8f02", size = 287988, upload-time = "2026-04-10T12:21:36.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/88/154179d492f2c000fe6efab3c3ff6b8eb94fbfaa09efe47999bce6b1e29f/cachebox-5.2.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:996f49d04b234082530afcc650bdd00556afbebc19c6c0daaafb85950340cb3c", size = 374245, upload-time = "2026-04-10T12:20:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9d/3b03f2e063161bcb1a5e0969d521b5c622c2da02252a5c8bd4ef0e4f9914/cachebox-5.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23a3300ebbb526fa12ce6fa53699002f5fba6da23b4bbbaf8ba8b18a3f03e6b3", size = 356308, upload-time = "2026-04-10T12:20:09.149Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9b/8da38af731e3832e9f987548e4bfb610d7f3054019e12c44a94ba9272b37/cachebox-5.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c63ee1589364caa04c018405e625d2e44e0bf9994f2715b2f322075d8c45b6", size = 395666, upload-time = "2026-04-10T12:18:51.89Z" }, + { url = "https://files.pythonhosted.org/packages/01/dd/1522aa808f94c904c5eb3640991799fed14dd43c1dd99a9f7b71bd95b1e3/cachebox-5.2.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebd0f8d4ebc3943c1ddcbbdc54f1a8ddf95505c862ed5731319cebd1eb98ae41", size = 353362, upload-time = "2026-04-10T12:19:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/95bf883ec9b69a76f3a7d9fb14d015d9a4bdab0143a3eff62ceebc8b1419/cachebox-5.2.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:569966efcc6309aa7d774443e3513cdbb8671efae0158138ba2ebb7d8cc9d8ed", size = 371007, upload-time = "2026-04-10T12:19:17.484Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/cc02066d5ccfcb8b35adbaf867977fdb54572cda56ace56da396f0caa3bf/cachebox-5.2.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5774d06f0da37dd566239a4376d6ca8cf983d3e4c3228712ec22b4130f662f21", size = 390670, upload-time = "2026-04-10T12:19:29.685Z" }, + { url = "https://files.pythonhosted.org/packages/b3/50/8e4d59b3e344405d8393d6cc5cc92754d3cc1d81134041ebffd3f5ab73e6/cachebox-5.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae5bf8755bc66bcf42e7ca5c42d703a041a7aaad58f9a0c3be54d5b1cefd2641", size = 395765, upload-time = "2026-04-10T12:19:56.169Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d4/d731cff1c4cec22404bd3ddda05b233c5efaa5f13d7abf4e2728905b7cdd/cachebox-5.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63f061cc6a5ca70bbce2e6be0588fe2fee00a93a1b0581b1086d54b10288cdb6", size = 425707, upload-time = "2026-04-10T12:19:42.714Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/3ec8aadceb0dcc66dbd0b9b32966cf7b6928ed84471424c24d21b0af62d0/cachebox-5.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577c781f18b559f4dc9eea176c6aed008843ef4b8e045cf61bb519e09dccc9ef", size = 564759, upload-time = "2026-04-10T12:20:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/31cbc8623ecc2e25900f7e8f20f11bfb84786989a59a8046e70b27cbea6d/cachebox-5.2.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7f691e25572a3ddbb018e19d796f774713bd6b0f7ce9be2e71f6e18572de264a", size = 669309, upload-time = "2026-04-10T12:20:51.117Z" }, + { url = "https://files.pythonhosted.org/packages/34/29/5a9e92bdc7b32dc865e73dd776638244f900136daee5bb0591a67e1530fa/cachebox-5.2.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:33368adf86669c29b936fbae5d6219cf90aacd4b1db71dae2e23d584a8219cd6", size = 643705, upload-time = "2026-04-10T12:21:05.882Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/5273a412855fdc11f674e4749aee6d5ec0a91f5c1a9f6e922f7fa0cb7a83/cachebox-5.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38ce67b7b45713e49459a09411d07f82de04022c04aecde6202cd32f934c2b1f", size = 609751, upload-time = "2026-04-10T12:21:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a4/0fadb5e6a00f373cc3fe56b4415cdea2fc0147f6ec475611762d16eb4b05/cachebox-5.2.3-cp311-cp311-win32.whl", hash = "sha256:a7cd2c81347063ab6c512d0f569aeb5f75fc2dfe686c8486258ffd08052324f4", size = 275485, upload-time = "2026-04-10T12:21:51.563Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/67c1bf83f815294d2c3acd7631f25b5cbe6067e1d56495f76829dd60057b/cachebox-5.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:7e45798d6b969794840bb302857946d710ecb32af78dfcb3ab40f4e68ee7fdaf", size = 288024, upload-time = "2026-04-10T12:21:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e7/6fa6abfc9c4c07b88f09a88466fa93c7081fd679d8e06f8f558bb4ac845c/cachebox-5.2.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09c0340e9daa7b4530801e5a570cb0c1a1ad941a85d245d360020d3986d0e787", size = 377791, upload-time = "2026-04-10T12:20:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/3a/79/89e4423352d0ca33bbf80fc1b4b665e654a93de8b16cf41e96fcac81801a/cachebox-5.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3162758792626685ec34950eedd565d015b115d0ff0d751d2716031fc32d51b", size = 359562, upload-time = "2026-04-10T12:20:10.626Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ab/e533c2751e6a3411ebe369277aaed03199b9e4586a48f0a3712a1f4b418b/cachebox-5.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a189a780c3ccd7b9d157074ba6bf3e191e522b39abbdb590075111851f02d50d", size = 397910, upload-time = "2026-04-10T12:18:53.336Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0d/b8492d6ca53278499a37c9f9d51afd4ad77bfbe813d6281944d45b97a1e7/cachebox-5.2.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:410b67baa99d433644199b11289627f7ebba4ee5786f95ca9858f238afcee157", size = 353699, upload-time = "2026-04-10T12:19:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/fd20b3a5362651303fa12d3ee62f56af2bd396e4a7303d7014a1a1e5b392/cachebox-5.2.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f81474dc19d3865fa5e57263f834bc6bbc00e471a594fb9d934ed552732c02fd", size = 372510, upload-time = "2026-04-10T12:19:18.997Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/3ec55c946d300cc4eaed3a0f79740051ac6e11ef4032421332c6ca15f5d5/cachebox-5.2.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85ccd827193b3e3e887a88a16b88ef7ed174e7e65be515b5253322aa75e665c3", size = 392802, upload-time = "2026-04-10T12:19:31.196Z" }, + { url = "https://files.pythonhosted.org/packages/01/b1/1a3c4e436ad8a4c4ba3e70f4c62e1f927cbbb3c943a9bba5813b8b815bde/cachebox-5.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a1e7d3cb8a5e7e68996a8619e3ef8771a124d14568c251f9e586eba88d759c1", size = 398223, upload-time = "2026-04-10T12:19:57.583Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/d36ad3976c4396b350b96a1582411b7a00e56c144eec0bb5ba5f36ce7d86/cachebox-5.2.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adcedfcfcb933b21e7fdcfe560c79887bc8287abceab0586aa3730417dd0277d", size = 427696, upload-time = "2026-04-10T12:19:44.361Z" }, + { url = "https://files.pythonhosted.org/packages/a8/36/71845b5c7a9ffbd85e6fdb470c11a174f499bd5238fa37b1214157c2454d/cachebox-5.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c7f0c72c51a3a9e7049ea6ff2a43cd3877ab7fee966eb65771a59621563b75e3", size = 567854, upload-time = "2026-04-10T12:20:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a2/baf0e5a8392e64e352b137ccd7356b3d98068c842fd19f510a7790c05d34/cachebox-5.2.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c48c10e498d573511aafbd545570e7f43b40a7428dc282183bf5adc334d9e1a8", size = 670306, upload-time = "2026-04-10T12:20:52.903Z" }, + { url = "https://files.pythonhosted.org/packages/a5/22/cd4e4c1d624b8ef9fb4b8bebf0bf5d2d74a399cf1ac46b667bb79d15359a/cachebox-5.2.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2f1e086ab5ffd082a68bb63699d517655a59b06414927bfc84e01df91b81e34d", size = 645943, upload-time = "2026-04-10T12:21:08.238Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d6/55859981f5ec6a9e412baaa4db6aa5973a00008750b3f054cdefcb6491fc/cachebox-5.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:649d18399f13735bb82daa33800196f815529c49e967767c40ca221723e68afa", size = 612309, upload-time = "2026-04-10T12:21:23.404Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1e/313f650467ac85824c4199188f8f1ee3386cd12eb665dbf7c88d372e4956/cachebox-5.2.3-cp312-cp312-win32.whl", hash = "sha256:0a17aeb4e5b1c6ef1c3db8fc5186f9986e215ba5ea5a5d08baa45bcf55f261b2", size = 279789, upload-time = "2026-04-10T12:21:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/c5/50/3b334f887accfa811cf5c7533b8ce22c523eb009363a86401198899dadd2/cachebox-5.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:cfd69114141ab362acaa2099e425a1b965cf7b021a539a4e953143d593930b74", size = 290917, upload-time = "2026-04-10T12:21:39.696Z" }, + { url = "https://files.pythonhosted.org/packages/31/3b/16d5c295f6ec2913ef595b39986dc7b7cc179fdd2e73f5ebd1814c38fd51/cachebox-5.2.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9527c5c70f8735f2d696331d8bcf77254f03b4dc8542046807823bd36ed4e8ba", size = 377408, upload-time = "2026-04-10T12:20:25.444Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/45f834154f79721e5b64a80ffab4f9710834c4f9c01fa977f94a9116c32a/cachebox-5.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40ac878af00d5969862c1f6bc076de1e34ca248662fce6aecca1761f52e33e32", size = 359274, upload-time = "2026-04-10T12:20:12.127Z" }, + { url = "https://files.pythonhosted.org/packages/46/17/794e5f93e0a172aa14ecd692f6d89bdf094f71eb35fa923d0a0af25cef1c/cachebox-5.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5ff26bfd8f7e95b3becf6d5f65c25edaca50fa68078868648b70d79bcccc260", size = 397520, upload-time = "2026-04-10T12:18:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/9470b1a96de6e480192b1a92b2fafa72aa052efc2509a5418a5652205b33/cachebox-5.2.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82e7002dd343afeeba2fcf0e483131b342a27ec3bc34b2214dc617691bda40d6", size = 353183, upload-time = "2026-04-10T12:19:07.797Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2b/72813f80397ed4640e337cbd1a14ab7eaafe33e479291d3623b6a6a55fec/cachebox-5.2.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ccbdc54a6c4b5758408c1083bdfa217bd382894a8331c7d0a54b84ba0cf51e5b", size = 372239, upload-time = "2026-04-10T12:19:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/47dc9687288fa55486573627089ecd9aae124de5924a4bce008af96d80b6/cachebox-5.2.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df5135a168f143d186b1cc3be0ca16b66446897ab5cedc03bd80bcc926fcd403", size = 392568, upload-time = "2026-04-10T12:19:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/13/95/450765b971a3bed9d7cf003c3833c1976482eb83b0241b6dbb840a25b43b/cachebox-5.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10bedf96db8f9766cc956f9adcc623e604264e5d6fa2e255432f8c2ed7519143", size = 397920, upload-time = "2026-04-10T12:19:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/dd8f4c1f92e58d479913ce9cbaa3227c911128e6046c82f4fd44309f685a/cachebox-5.2.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f22732d0d69bb84ad2dca7480bffdfd0430c647152d488936e152ecbbfee52fb", size = 427332, upload-time = "2026-04-10T12:19:45.888Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/80d8c26ce63e78da3874a5bb07a3a78de53a2b0356ba80583a4927f0a074/cachebox-5.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:26ae0b68979204d360327f4c0725cfdc95cfc34ab73ab1a8f528e3bd2f6d023c", size = 567494, upload-time = "2026-04-10T12:20:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/10/35/7249885dfed3602b3b48c1e67781197dcdc536c50f72caeabe3944348af8/cachebox-5.2.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f3d628b816e28a6e7661d460e02dd5b421247cc2cd275814f80ea79621245fc4", size = 669968, upload-time = "2026-04-10T12:20:55.155Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8a/e5b58f0bbd6fef74da5d8e5ab49e67898ce7e6df28c16280a0f2b78461f7/cachebox-5.2.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:64057caa6b741320655cd3c5997fe642dae5dbff571eb530e6f53e58272bb43b", size = 645547, upload-time = "2026-04-10T12:21:09.948Z" }, + { url = "https://files.pythonhosted.org/packages/d8/25/51783a4c6f25ca87ef1b4b762ff0364bd98053a02d597b30d26ff4cf13c5/cachebox-5.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa325306084aa2dc0b21e07723d7700f4d43dece3732c7fdaf7a269dc5e35aa7", size = 611844, upload-time = "2026-04-10T12:21:25.286Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/b26c4b046e296d0e249448fe297626b3caca2e851837712f03c358662cb7/cachebox-5.2.3-cp313-cp313-win32.whl", hash = "sha256:55003089d21c2f5515089c307be063b45558e884a4a1cc9593944374c89975c4", size = 279421, upload-time = "2026-04-10T12:21:54.921Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7f/a49420670393bfea618de7a893d45cae9294cf3293d7b158e7af20e8f39e/cachebox-5.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:dcc5edb6ecf2b516e90b773d232360c5e4ed8fdcda038b19441da2ed9cf208ab", size = 290702, upload-time = "2026-04-10T12:21:41.458Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/bf83bda13ef6fc490d208a1d4dd712034624526a88f61713cca0edc9884f/cachebox-5.2.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a4b7559fa4994c4032dd07466c2041d57e055feb814762e1f73f4e8beef188d0", size = 371704, upload-time = "2026-04-10T12:20:27.253Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ea/aa5162273238e84f9e41b33600c69299572dc1c8f0f768d07660b71be07d/cachebox-5.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f57afada3d9327adf87f3b5cf0094348c6fd49354ab2e9bd20b044648eb094ae", size = 353385, upload-time = "2026-04-10T12:20:13.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/96/3ca013e2e48df5c1d7855669b208f4bf8014ccb842ccf7a3a0eaac07bee0/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8342ff350ce86f062492752d612e9f056ac5dc56375713d75c3bf6e83b4d18db", size = 392181, upload-time = "2026-04-10T12:18:56.385Z" }, + { url = "https://files.pythonhosted.org/packages/63/ca/1bacb4efa0b0ce8065d1fb7c8dc7c382ec4e1cc3f007eb08417732be2725/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:405f9cc8492fc9d953b5a6b9e2b661e99583755c6639ab8d09a287fdf336503c", size = 349494, upload-time = "2026-04-10T12:19:09.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2e/75db4bda3768658f5baa5a54f6a4f643bc2de1a16788e40581a080e803c7/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94aae393ec1d9b26565d346445bb6afa3963d2a0d3eb5e4188d0e510fab871a0", size = 369216, upload-time = "2026-04-10T12:19:22.224Z" }, + { url = "https://files.pythonhosted.org/packages/f5/82/e1f833be0d57e29a8c5eb0a0275cd34b962f3c7f5b9e0517ec4bf75e7cc3/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8b0b575066fc09f6fae0d4bd30d6ff56584a6870cbe7d202916c5e0d725cfd4", size = 385922, upload-time = "2026-04-10T12:19:34.198Z" }, + { url = "https://files.pythonhosted.org/packages/53/d6/615a3c16c1d63839f2c67644eb414c4dc9769ab2e169d935110fd8e268d5/cachebox-5.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41e99c1240106d39b63ce7868a6cd8c9da9243fef08848b85d428164e0769fd2", size = 393276, upload-time = "2026-04-10T12:20:00.925Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/7844c9c84b170dae1005b22da174639968e64c8055d66a209a1598663771/cachebox-5.2.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:432ca62b99f7eafc21af669d76c88c1b7377db179b89fb6fca3ea93b8f9fff19", size = 421355, upload-time = "2026-04-10T12:19:47.691Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/43f62355846cae3dc41cb4daccac0a4bb2b7b8b3c7d77d1b6a220bae6d54/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e51d9c59006b53447f806145406eb37a7fc3c25553d4fd24c3887f3b268d214e", size = 561656, upload-time = "2026-04-10T12:20:42.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fc/a453813c6d000d69a41a06c6a3143a6c4d0d0e41f23c155db2f82ea0edfa/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:5e48a405f699fb001b8af120a6e0b4a981277f84eb5dd66a1faa21e4b6fe9485", size = 665791, upload-time = "2026-04-10T12:20:56.842Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a3/f6a9e75f1e602b67b6d67088a9a766adfc4e0a740a9c4b68e4e6207c1006/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8cbfc007ea78af61d75d7d26e5854df53dc5da6877d074afd4b4696c074f4ee7", size = 640975, upload-time = "2026-04-10T12:21:11.641Z" }, + { url = "https://files.pythonhosted.org/packages/a3/15/4ac98277f7fd9d855c8ed337e8e2a3386d17997cce2dd3eadb23dedc08e3/cachebox-5.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6a94d0da8133b3a0707ae11c9ea321f8fc37e3b5a14517019a05d632218b0f56", size = 607242, upload-time = "2026-04-10T12:21:27.27Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0b/ce61907a803f75854e0cc91b84c16e14dce0e4e939efbda26293eb4c8784/cachebox-5.2.3-cp313-cp313t-win32.whl", hash = "sha256:5fee33549877c03c2494ec5359a57a7667f872fe8e296a7f39d3dfe08dd3914c", size = 271619, upload-time = "2026-04-10T12:21:56.768Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/fece190ad5173d06b2779494aaad5528907f2e55c809618e5b67c2e3dbb5/cachebox-5.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:67548a05cd41fcc4f7af80a2f97f742fef3d436537ac2e1a1dce0fcba5d41190", size = 283133, upload-time = "2026-04-10T12:21:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8b/72c0e80aad08e09867ce14a621bce689a733552f20cdf2ef96d4b052da10/cachebox-5.2.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:37fa0891f0defee053c09f5f43f802f731e36e6e6ca055d7d174af07f77232ca", size = 380523, upload-time = "2026-04-10T12:20:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fc/62/33aaade81b181d5191cc39c867c297aa7c65f3191aa9749bf99b77496b88/cachebox-5.2.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dc6315902f2ef4afbf10bc8e08c54ff34de5ce124546b8e0016c9b0d327be21e", size = 362424, upload-time = "2026-04-10T12:20:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0b/3eedaf9ea4b41c931f4340bfa42056efe2bb5fe3a79649d6c8a1dce585a5/cachebox-5.2.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7df1735ca778480d51b8232fed397ffe3935158f20d34fb1c5ed171b53d5a6e2", size = 399572, upload-time = "2026-04-10T12:18:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/be/69/c79b8a6a5b889ac4a60800bacea3553cb3b86f6fd13b2262bade1cb962c6/cachebox-5.2.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e22451cde8f884051e941b21870e4fc91fcf58d0d8c285bb8964107e1f02445c", size = 353803, upload-time = "2026-04-10T12:19:11.21Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c3/bc7838de51039f8c50506d8dc82f22ff9a652794339a223b12af595e1d2f/cachebox-5.2.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dcbccf3015d9a42bcf41260fa5cc048a5bdb75aa10997d514d6c976117f30ee2", size = 374474, upload-time = "2026-04-10T12:19:23.658Z" }, + { url = "https://files.pythonhosted.org/packages/65/61/e5231ad2ae952ca482f9b9df55df4b96add1a80de28de537c5f574605987/cachebox-5.2.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:311eae5079e256cbbfafdc3dcff1714b6598a767f9c1ef8c3709e74ea0cc12b0", size = 393045, upload-time = "2026-04-10T12:19:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c9b3fa764ac5420a9e079ad53fa8840d4a26b74c4ccda56acbef49cf76ff/cachebox-5.2.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f4d2a80a5cd3380739c67f7d89e596634f5897b8d5a4a3dc1598312cb077535", size = 398700, upload-time = "2026-04-10T12:20:02.513Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3e/c4e3acd4cb04e01c5fb7cc7a4de16059b9594d90672fff85af8670275267/cachebox-5.2.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3977515b727a5203f494c44c4566fb936c4b940351c01d3d8e7b5d104dff4f53", size = 426725, upload-time = "2026-04-10T12:19:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/610b79479719951581109d985244d34c97f86a308c3d7c83443e2b1dac46/cachebox-5.2.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c5be17dd5c4fabcfecd5bcf6d54f9c6fb719daed3ef01ac1c03a14af0e2b26c1", size = 570042, upload-time = "2026-04-10T12:20:43.793Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/cad8a05db4d0c0f5ba6bccb32e57d15c472276de9476f56004445b40711f/cachebox-5.2.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6d37334fc218fdaee31db8a4f938938716e7c3b1b4059e25de27c8447fc95fde", size = 670974, upload-time = "2026-04-10T12:20:58.528Z" }, + { url = "https://files.pythonhosted.org/packages/54/d1/9cff7c2b9048d1c38b7ad8199ce856596d09720b3bea74043f3bad71970b/cachebox-5.2.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1e5f1b7e23411b748d919348c3b65db1f9f8927ab8f6f3acae19bd617543df2d", size = 646213, upload-time = "2026-04-10T12:21:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/27/ae/2e1ad162ec13903e84469c8a753baf385f1bc324279d6c7cb6365e7099df/cachebox-5.2.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7b06a75a898b31fd73c4d8bf727a9b9f8b5b7738cccd0ab5e6fd2a9cf659d3c", size = 612787, upload-time = "2026-04-10T12:21:29.271Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8a/07b5ffd841e1ff534bb6e8721c39fdfe0d7cdaac1398e1783b2a0c37bd22/cachebox-5.2.3-cp314-cp314-win32.whl", hash = "sha256:3b798052719f09a2ce7bf9fa9452dc0a7d4dc53b50a2d3aba6ce6ebc12d39df7", size = 278559, upload-time = "2026-04-10T12:21:58.482Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/b88a82ce9ec7a2fa0f09ed1cdd031692c8664c41f9ab71831e177c7ce2df/cachebox-5.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:4afc8b8575e3228a42ad8d819de5fbbecc6bd0b521295966b00244be37ae3b9b", size = 291928, upload-time = "2026-04-10T12:21:44.621Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/8c79c07c8c6517fb2fe7d479dd87044e38aac5b9af0245b33fcd695eae37/cachebox-5.2.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:0e8a34b82be30d3d9fb7dfaf9a86ec2b3ab9bc264715909ef27fc3d3587324d2", size = 374325, upload-time = "2026-04-10T12:20:30.923Z" }, + { url = "https://files.pythonhosted.org/packages/7f/51/0fc26b923e80ab857ac99d5f7f3784dc941e7b4de361c204835233176ddf/cachebox-5.2.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4d4e336aebf866463878ccd28a4d0ef4003ea216708cf4a02a7f198481b3af81", size = 355444, upload-time = "2026-04-10T12:20:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6d/a6b399221f8dc4b3e01b37d3240ef5b8a7eb78cd9bfbb99b0e655dd01649/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b102fcdd97b0602bf5d6ba1a571bba3e3d6fa912b89fd768b0da5427408eab8", size = 393978, upload-time = "2026-04-10T12:18:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f1/4c8f998c117c1941a82bd824d6687280c50167f21fea6392e41531d641e2/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:245a79fb2c5d3bff252f4263f76210ef3ad7c2ff9b0234859b26974830a80491", size = 349298, upload-time = "2026-04-10T12:19:12.843Z" }, + { url = "https://files.pythonhosted.org/packages/d1/dd/683bc5a32a0da660d02fa248b880b71a2b834e9b54b8d272b5801282f402/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd0e8dbd8fd4cf664c645c08f9e10508e133353756705c4a738e90a5406224b5", size = 370619, upload-time = "2026-04-10T12:19:25.298Z" }, + { url = "https://files.pythonhosted.org/packages/81/49/d6c47c78a7769b355076c5b635c2b538c8b88e8ceeb408e104d0f269b515/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdb74294bdc33e39e26606919a9b2229038d5fac0edb80c9056683c08584d4a9", size = 385988, upload-time = "2026-04-10T12:19:37.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/e2/b669555ada7fa1392e4cdb8a19f3367db5c6abef0fde8ab034a9747760df/cachebox-5.2.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bba3e9a7f52fa196b434522f39675f3b32a076976ef2373ded6f1065e99f4d20", size = 394090, upload-time = "2026-04-10T12:20:03.978Z" }, + { url = "https://files.pythonhosted.org/packages/8f/01/42916249e53fe4fcbdf0419fb55dbc09b9f377475376e1d7f4ae9c9bd6cd/cachebox-5.2.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abb21f0f937fb66528f1b9f1a04874d6aa503e78bbb26f4cf33bf67faddbdd68", size = 421632, upload-time = "2026-04-10T12:19:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/a1/54/34eebe18c6ed8ba27b1331b5e3d08bd8bb62f03ba81fbf47a2db0fa646f7/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dab6fd3189b0c746fb03e1915fd947aaca9112cedf26ef3a0c39383acf87d2e5", size = 563871, upload-time = "2026-04-10T12:20:45.417Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b2/f92da0d54e4f18609588709090de8c81dd7c8b20ed6ac30f9b91bedbedf5/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e7d2935b9df11d3717f99c7237b6780f1f8c70e6a99b69b8430d89929ec825", size = 665677, upload-time = "2026-04-10T12:21:00.512Z" }, + { url = "https://files.pythonhosted.org/packages/43/9d/bf2d3dc949afe4d21fc7eb15b7524255e834b9252df6bba111e6686d1c6f/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:611aa260fe1b2506330ff72f415e2cb4053c9c4e3776ac68fe2eedee0e1b91b1", size = 642067, upload-time = "2026-04-10T12:21:15.727Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4f/a789eda189550d239fbaf165b9810f148e733e97a2a4eda7c4192295c7f8/cachebox-5.2.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a424ffb8514a9cb49bacff7995b7c767625cb2239692bd6524245e8579e375cc", size = 608048, upload-time = "2026-04-10T12:21:31.156Z" }, + { url = "https://files.pythonhosted.org/packages/41/c3/590e161c04ffbd36e33933e6dcca5ffa40b5548e3121a21d77aad42af138/cachebox-5.2.3-cp314-cp314t-win32.whl", hash = "sha256:83988dd8e9075ee837e8407e26db49a9944ae74924d5db57b477444d7d98622c", size = 271694, upload-time = "2026-04-10T12:22:00.589Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/f60b8506df467261178afe918801df37c02c46ec2b8ce019760a14e2abe7/cachebox-5.2.3-cp314-cp314t-win_amd64.whl", hash = "sha256:dbda6390fa5070a19157ae35ab8066d3fe468634e0e9e21452c68ce7999c7d0c", size = 284212, upload-time = "2026-04-10T12:21:46.241Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7b/5eead1ca0d437b1993a742c6571079ae58ae4db50d94d42e87b514aed6c3/cachebox-5.2.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c798cddfb780156db09d3d96ed5da4c2d5fc01dad4bc7b54db5b20c34f221926", size = 376199, upload-time = "2026-04-10T12:20:32.674Z" }, + { url = "https://files.pythonhosted.org/packages/77/e3/5e45042f9b552a5087cafc2e0fed834e632531fca17818201d72e78593ce/cachebox-5.2.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c8f3de4afeb3fd721620be3d02f2338bcbc3fdbd464ca14e1c474088c9669db0", size = 357109, upload-time = "2026-04-10T12:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/d4/51/3c4743b718b42e4b80166fa61f8722b603eba7bf206768a7892c4699dce7/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39022c258872185327acffa9ad42d6bdf42f37d006d35c825a684eb5fa98d40", size = 396433, upload-time = "2026-04-10T12:19:01.463Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/678da91187bdb2836db2b8da62519da75359b46bc28697799a7caa314519/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a0599fb85dcb6df9a86502435643fe90c793bbcd50b5d85217c70f2bc2e38fc", size = 354287, upload-time = "2026-04-10T12:19:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/df/06/769446da6c9f2855499aaa19e2d7260aa47934bc2e15a931e5b737f8685a/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3cdbe8f1b7716a44dc82ef3a6830a612260c7379478cfa80804632e2e6252b8e", size = 372507, upload-time = "2026-04-10T12:19:26.763Z" }, + { url = "https://files.pythonhosted.org/packages/79/cf/86c60994a7be734abef0395e440dc11714f84ffcd369cbcd8e61c3d58126/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:783d1b9a0b3c77c43e7ae331b9d6561ad75827e16b2484e2a6cc289ec4d392ee", size = 390831, upload-time = "2026-04-10T12:19:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/9d/db/acfb55f8d5ee4ea1c5f2d32ede25d4d04e944ba09d2832c27c085022490d/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c6476a2a842906fee782d92f8fbcb03ecfd22eecc39adb7fb5b047d7e1cf020", size = 396277, upload-time = "2026-04-10T12:20:05.735Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4f/35e27e85a48e15671c5863addcabde910eb311800a621c3e47c04bd36d17/cachebox-5.2.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:184bbcfa1370415b6d1f09e4fb74ab697dac8df09f522aa217a2fac65f973744", size = 426980, upload-time = "2026-04-10T12:19:52.622Z" }, + { url = "https://files.pythonhosted.org/packages/09/4b/50f2cadf20c02db9e449f2e9fee95f3eb5768ab1804dd0a5eba6c98119ad/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f89df36b46f8f5e11c0c49701ec3cebddf51191f96afb7bb75c394faf3c1cbc8", size = 565539, upload-time = "2026-04-10T12:20:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/43/53/b8e948cadb48b8bcf1d13c2aa4a788ff0e95b50ddb808c18e998499b4680/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:fb0bdcd9e28686e3b91d5210c843542858f0f10de151181aee27a7978fe4992e", size = 670870, upload-time = "2026-04-10T12:21:02.141Z" }, + { url = "https://files.pythonhosted.org/packages/29/7b/d68ca3f59a9d6963c2f6b19bc4b1926a37db2e4a4f6c9891d12788e49ce2/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:5196f0d2c2f99c92ddf0d2c37803ff90509d14a5df211b7754feb8b61ffd8740", size = 644542, upload-time = "2026-04-10T12:21:17.541Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c8/44ae6d5dff09f044d61a92591e6a8db17f3b2ee51a54d375cce90271527b/cachebox-5.2.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:73671850d8c3634ab217398c83715d3feb52589ec97bd8e2f4d22e472741ea48", size = 610235, upload-time = "2026-04-10T12:21:32.93Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/31cf2449da9a296f6c6c0002c7ae91a25c3a4bfef071763bbeb85300b402/cachebox-5.2.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:70c718f6bb77e6ba142b9a055b81ce85412a0c0e5e82a154489b45e6f91d09ec", size = 287614, upload-time = "2026-04-10T12:21:47.909Z" }, ] [[package]] @@ -1641,14 +1760,14 @@ wheels = [ [[package]] name = "click" -version = "8.1.8" +version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, ] [[package]] @@ -1763,7 +1882,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -1842,115 +1961,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" +version = "7.14.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } 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" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { 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" }, + { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, + { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/202235eb5c3c14c212462cd91d61b7386bf8fc44bc7a77f4742d2a69174b/coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed", size = 252633, upload-time = "2026-05-10T17:59:53.244Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/5f596e8995785124ee191c42535664c5e62c65995b66f4ca21e28ae04c81/coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980", size = 254743, upload-time = "2026-05-10T17:59:55.021Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6d/0d178825be2350f0adb27984d0aa7cf84bbdab201f6fb926b535d23a8f5f/coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0", size = 256700, upload-time = "2026-05-10T17:59:56.511Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/9e549c2f6e9dfea472adadba06c294e64735dabc2dd19015fac082095013/coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742", size = 250854, upload-time = "2026-05-10T17:59:57.94Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1c/b94f9f5f36396021ee2f62c5834b12e6a3d31f0bed5d6fc6d1c3caec087c/coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5", size = 252433, upload-time = "2026-05-10T17:59:59.688Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cb/d192cd8e1345eccabc32016f2d39072ecd10cb4f4b983ed8d0ebdeaf00dc/coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327", size = 250494, upload-time = "2026-05-10T18:00:01.953Z" }, + { url = "https://files.pythonhosted.org/packages/53/c5/aac9f460a41d835dbddef1d377f105f6ac2311d0f3c1588e9f51046d8813/coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d", size = 254261, upload-time = "2026-05-10T18:00:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/23/aa/7af7c0081980a9cb3d289c5a435a4b7657dcecbd128e25c580e6a50389b5/coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20", size = 250216, upload-time = "2026-05-10T18:00:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/a4257538ce2f6b978aeb51870d6c4208c510928a03db7e0339bb625dccb7/coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c", size = 251125, upload-time = "2026-05-10T18:00:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ab/f91af47642ec1aa53490e835a95847168d9c77fc39aa58527604c051e145/coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3", size = 222300, upload-time = "2026-05-10T18:00:08.608Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a71ddbd874431e7a7cd96071f0c331cfbbad07704833c765d24ffbab8a67/coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1", size = 223241, upload-time = "2026-05-10T18:00:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/d8/6e/d9d312a5151a96cd110efee32efc3fc97b01ebd86203fe618ccb29cf4c92/coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627", size = 221908, upload-time = "2026-05-10T18:00:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/09/1e/2f996b2c8415cbb6f54b0f5ec1ee850c96d7911961afb4fc05f4a89d8c58/coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5", size = 219967, upload-time = "2026-05-10T18:00:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662", size = 220329, upload-time = "2026-05-10T18:00:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/75/cf/a8f4b43a16e194b0261257ad28ded5853ec052570afef4a84e1d81189f3b/coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f", size = 251839, upload-time = "2026-05-10T18:00:17.16Z" }, + { url = "https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67", size = 254576, upload-time = "2026-05-10T18:00:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/22/ec/c936d495fcd67f48f03a9c4ad3297ff80d1f222a5df3980f15b34c186c21/coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9", size = 255690, upload-time = "2026-05-10T18:00:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5af63f636cc62a4a2b1b3ba9146f6ee6f53a35a50d5cefc54d5670f60999/coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb", size = 257949, upload-time = "2026-05-10T18:00:22.28Z" }, + { url = "https://files.pythonhosted.org/packages/26/d3/a225317bd2012132a27e1176d51660b826f99bb975876463c44ea0d7ee5a/coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e", size = 252242, upload-time = "2026-05-10T18:00:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7f/9e65495298c3ea414742998539c37d048b5e81cc818fb1828cc6b51d10bf/coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3", size = 253608, upload-time = "2026-05-10T18:00:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/94/46/1522b524a35bdad22b2b8c4f9d32d0a104b524726ec380b2db68db1746f5/coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4", size = 251753, upload-time = "2026-05-10T18:00:27.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/cdf00d38817742c541ade405e115a3f7bf36e6f2a8b99d4f209861b85a2d/coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1", size = 255823, upload-time = "2026-05-10T18:00:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/38/fc/5e7877cf5f902d08a17ff1c532511476d87e1bea355bd5028cb97f902e79/coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5", size = 251323, upload-time = "2026-05-10T18:00:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/18/9d/50f05a72dff8487464fdd4178dda5daed642a060e60afb644e3d45123559/coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595", size = 253197, upload-time = "2026-05-10T18:00:32.211Z" }, + { url = "https://files.pythonhosted.org/packages/00/3f/6f61ffe6439df266c3cf60f5c99cfaa21103d0210d706a42fc6c30683ff8/coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27", size = 222515, upload-time = "2026-05-10T18:00:33.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2", size = 223324, upload-time = "2026-05-10T18:00:35.172Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/9f7fe62f659f24b7a82a0be56bf94c1bd0a89e0ae7ab4c668f6e82404294/coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d", size = 221944, upload-time = "2026-05-10T18:00:37.014Z" }, + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, ] [package.optional-dependencies] @@ -2054,14 +2173,15 @@ wheels = [ [[package]] name = "deepdiff" -version = "9.0.0" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cachebox", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "orderly-set", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/20/63dd34163ed07393968128dc8c7ab948c96e47c4ce76976ea533de64909d/deepdiff-9.0.0.tar.gz", hash = "sha256:4872005306237b5b50829803feff58a1dfd20b2b357a55de22e7ded65b2008a7", size = 151952, upload-time = "2026-03-30T05:52:23.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/6b/6a4a5aaf38535eb332c2856aa08e73ed7c549d0851b1215401af0a2db1a7/deepdiff-9.1.0.tar.gz", hash = "sha256:07e9e366fab4297755153c4eab795ad4ef3cbd0d51660e847f5751c6bd727687", size = 382149, upload-time = "2026-05-15T20:18:05.751Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/c4/da7089cd7aa4ab554f56e18a7fb08dcfed8fd2ae91fa528f5b1be207a148/deepdiff-9.0.0-py3-none-any.whl", hash = "sha256:b1ae0dd86290d86a03de5fbee728fde43095c1472ae4974bdab23ab4656305bd", size = 170540, upload-time = "2026-03-30T05:52:22.008Z" }, + { url = "https://files.pythonhosted.org/packages/c7/26/4a2bad8eb430d8d805a4642c4bff25103a37548d74ab346f8b1e024abcc5/deepdiff-9.1.0-py3-none-any.whl", hash = "sha256:80c0460e1993b04f6f0ca79abf25548b129fd218478c4ebb08f80560f5d10610", size = 184662, upload-time = "2026-05-15T20:18:03.956Z" }, ] [[package]] @@ -2318,59 +2438,59 @@ wheels = [ [[package]] name = "fonttools" -version = "4.62.1" +version = "4.63.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, - { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, - { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, - { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, - { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, - { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, - { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, - { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, - { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, - { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, - { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, - { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, - { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, - { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, - { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, - { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, - { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, - { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, - { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, - { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, - { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, - { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, - { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, - { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, - { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, - { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, - { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, - { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, - { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] [[package]] @@ -2523,11 +2643,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.3.0" +version = "2026.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, ] [[package]] @@ -2578,15 +2698,15 @@ wheels = [ [[package]] name = "google-auth" -version = "2.49.2" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyasn1-modules", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, ] [package.optional-dependencies] @@ -2596,7 +2716,7 @@ requests = [ [[package]] name = "google-genai" -version = "1.73.1" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2610,21 +2730,21 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/d8/40f5f107e5a2976bbac52d421f04d14fc221b55a8f05e66be44b2f739fe6/google_genai-1.73.1.tar.gz", hash = "sha256:b637e3a3b9e2eccc46f27136d470165803de84eca52abfed2e7352081a4d5a15", size = 530998, upload-time = "2026-04-14T21:06:19.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/59/3ed61240ef20b3ae6ed54e82c6f8b6d1f194947bc6679679dd6cdb037594/google_genai-1.75.0.tar.gz", hash = "sha256:56bac3991b311c93f980c0a2abcd287b672146905df1fbd71c92ed633d5a07cf", size = 539039, upload-time = "2026-05-04T22:48:54.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/af/508e0528015240d710c6763f7c89ff44fab9a94a80b4377e265d692cbfd6/google_genai-1.73.1-py3-none-any.whl", hash = "sha256:af2d2287d25e42a187de19811ef33beb2e347c7e2bdb4dc8c467d78254e43a2c", size = 783595, upload-time = "2026-04-14T21:06:17.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b6/552d40e96da22921eb1fead7c14b00b5b5473a20e45959488660fab35ee2/google_genai-1.75.0-py3-none-any.whl", hash = "sha256:8dc4c096e7d6288c3087f6893f582fe52468932464781edb8193bd92b9fefb2c", size = 793726, upload-time = "2026-05-04T22:48:53.033Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.74.0" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] @@ -2691,15 +2811,12 @@ wheels = [ ] [[package]] -name = "griffe" -version = "1.15.0" +name = "griffelib" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] [[package]] @@ -2799,34 +2916,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.4.3" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, - { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, - { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, - { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, - { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, ] [[package]] @@ -2853,11 +2970,11 @@ wheels = [ [[package]] name = "httpdbg" -version = "2.1.6" +version = "2.1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/47/69b5ceb2bbd03657b0a1458d8f3fdccd331d05bfa139569e6f293a2c4353/httpdbg-2.1.6.tar.gz", hash = "sha256:7f0925718faa0c94f5855075b168dc95bb16f9cd6826eb8449c8af9e720ea42b", size = 80616, upload-time = "2026-03-28T11:04:27.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/a2/5ccf3dab272bf8b07b5211da5748bec75b231d7c3471afd8f7f2a399289d/httpdbg-2.1.7.tar.gz", hash = "sha256:6ee7db35ad4d5d71cc75b1d5088567e4f71a65b987dfd934c8ac7d1d6cb6cc26", size = 80657, upload-time = "2026-05-01T10:34:34.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a8/6d101e4d58563e8647fe010a5ae61ba41ceb61e2abd789170573599d7d43/httpdbg-2.1.6-py3-none-any.whl", hash = "sha256:e3d5be9cd5eeb262b77e4faf113c356b7da127bb848b3360c99e394bb2dc9590", size = 87938, upload-time = "2026-03-28T11:04:26.035Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/46ab6052e3cae4489087b3c8bda83bc8b4911a126339cdd249f72fb6ab30/httpdbg-2.1.7-py3-none-any.whl", hash = "sha256:e29578bfdca82361805adc19938e64fcabcf5029a9c9783757b1ffac2a4dc2f8", size = 87966, upload-time = "2026-05-01T10:34:32.707Z" }, ] [[package]] @@ -2934,7 +3051,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.12.2" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2947,9 +3064,9 @@ dependencies = [ { name = "typer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/9f/3fda8b014db3ae239addc9b48b35c2cf7d318950b430712f34a2473ef81d/huggingface_hub-1.12.2.tar.gz", hash = "sha256:282c4999e641c89affdc4c02c265eddea944c1390dc19e89dac8ad3ae76dbdaf", size = 763393, upload-time = "2026-04-29T09:45:09.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/b6/e22bd20a25299c34b8c5922c1545a6320825b13906eb0f7298edfd034a0b/huggingface_hub-1.15.0.tar.gz", hash = "sha256:28abfdddda3927fd4de6a63cf26ab012498a2c24dae52baf150c5c6edf98a1d5", size = 784100, upload-time = "2026-05-15T11:42:52.149Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/c1/1fa4162f6dd53259daf2ad31385273341821fa0acce164cd03971937a60e/huggingface_hub-1.12.2-py3-none-any.whl", hash = "sha256:7968e897fdbc6343c871c240d87d4434efe0ad9f80d57daa1cc5678c6d148529", size = 647757, upload-time = "2026-04-29T09:45:07.63Z" }, + { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, ] [[package]] @@ -3015,23 +3132,23 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] @@ -3075,105 +3192,105 @@ wheels = [ [[package]] name = "jiter" -version = "0.14.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -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" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/2e/a9959997739c403378d0a4a3a1c4ed80b60aeace216c4d37b303a9fc60a4/jiter-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:02f36a5c700f105ac04a6556fe664a59037a2c200db3b7e88784fac2ddf02531", size = 316927, upload-time = "2026-04-10T14:25:40.753Z" }, - { url = "https://files.pythonhosted.org/packages/27/72/b6de8a531e0adbadd839bec301165feb1fccf00e9ff55073ba2dd20f0043/jiter-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41eab6c09ceffb6f0fe25e214b3068146edb1eda3649ca2aee2a061029c7ba2e", size = 321181, upload-time = "2026-04-10T14:25:42.621Z" }, - { url = "https://files.pythonhosted.org/packages/db/d8/2040b9efa13c917f855c40890ae4119fe02c25b7c7677d5b4fa820a851fc/jiter-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf4d4c109641f9cfaf4a7b6aebd51654e405cd00fa9ebbf87163b8b97b325aa", size = 347387, upload-time = "2026-04-10T14:25:44.212Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/655c0ad5ce6a8e90f9068c175b8a236877d753e460762b3183c136db1c5b/jiter-0.14.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80c7b41a628e6be2213ad0ece763c5f88aa5ee003fa394d58acaaee1f4b8342", size = 373083, upload-time = "2026-04-10T14:25:45.55Z" }, - { url = "https://files.pythonhosted.org/packages/f1/66/549c40fa068f08710b7570869c306a051eb67a29758bd64f4114f730554c/jiter-0.14.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3dbf7cc0d4dbe73cce307ebe7eefa7f73a7d3d854dd119ea0c243f03e40927", size = 463639, upload-time = "2026-04-10T14:25:47.452Z" }, - { url = "https://files.pythonhosted.org/packages/25/2f/97a32a05fed14ed58a18e181fdfb619e05163f3726b54ee6080ec0539c09/jiter-0.14.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7054adcdeb06b46efd17b5734f75817a44a2d06d3748e36c3a023a1bb52af9ec", size = 380735, upload-time = "2026-04-10T14:25:49.305Z" }, - { url = "https://files.pythonhosted.org/packages/2a/3b/4347e1d6c2a973d653bbb7a2d671a2d2426e54b52ba735b8ff0d0a29b75c/jiter-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d597cd1bf6790376f3fffc7c708766e57301d99a19314824ea0ccc9c3c70e1e2", size = 358632, upload-time = "2026-04-10T14:25:50.931Z" }, - { url = "https://files.pythonhosted.org/packages/ef/24/ca452fbf2ea33548ed30ce68a39a50442d3f7c9bf0704a7af958a930c057/jiter-0.14.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:df63a14878da754427926281626fd3ee249424a186e25a274e78176d42945264", size = 359969, upload-time = "2026-04-10T14:25:52.381Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a3/94470a0d199287caabeb4da2bb2ae5f6d17f3cf05dfc975d7cb064d58e0f/jiter-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ea73187627bcc5810e085df715e8a99da8bdfd96a7eb36b4b4df700ba6d4c9c", size = 397529, upload-time = "2026-04-10T14:25:53.801Z" }, - { url = "https://files.pythonhosted.org/packages/cf/71/6768edc09d7c45c39f093feb3de105fa718a3e982b5208b8a2ed6382b44b/jiter-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9f541eaf7bb8382367a1a23d6fc3d6aad57f8dd8c18c3c17f838bee20f217220", size = 522342, upload-time = "2026-04-10T14:25:55.396Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6b/5c2e17559a0f4e96e934479f7137df46c939e983fa05244e674815befb73/jiter-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:107465250de4fce00fdb47166bcd51df8e634e049541174fe3c71848e44f52ce", size = 556784, upload-time = "2026-04-10T14:25:56.927Z" }, - { url = "https://files.pythonhosted.org/packages/b1/83/c25f3556a60fc74d11199100f1b6cc0c006b815c8494dea8ca16fe398732/jiter-0.14.0-cp310-cp310-win32.whl", hash = "sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10", size = 208439, upload-time = "2026-04-10T14:25:58.796Z" }, - { url = "https://files.pythonhosted.org/packages/2e/99/781a1b413f0989b7f2ea203b094b331685f1a35e52e0a45e5d000ecaab27/jiter-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb8b682d10cb0cce7ff4c1af7244af7022c9b01ae16d46c357bdd0df13afb25d", size = 204558, upload-time = "2026-04-10T14:26:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, - { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, - { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, - { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, - { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, - { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, - { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, - { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, - { 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/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, - { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, - { 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" }, + { url = "https://files.pythonhosted.org/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" }, + { url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" }, + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, ] [[package]] @@ -3214,7 +3331,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.23.0" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3222,9 +3339,9 @@ dependencies = [ { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] @@ -3365,7 +3482,7 @@ wheels = [ [[package]] name = "langfuse" -version = "4.5.1" +version = "4.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3377,99 +3494,99 @@ dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/bd/9b12c9dd3ae1883619b20daa6d60f20a780ce2d25564d9b2168db27cbeb0/langfuse-4.5.1.tar.gz", hash = "sha256:fe8f9219f4101c0921934b0aeb1b45834f8e7d248e5f830b2c89c5b40aea6d83", size = 279735, upload-time = "2026-04-24T15:21:43.976Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/31/4b7157be23e7c8c3581ac5f6547c5c003e232e7044c92398c468ef78a809/langfuse-4.6.1.tar.gz", hash = "sha256:7f256c669e610909c2e93ca3e9e4168dbef344b753b6874f14b0edd673863f17", size = 281379, upload-time = "2026-05-08T14:08:15.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/63/77bd7220dfd60885a272a851f780b3f83e0f653ee3a852347552c3e24a28/langfuse-4.5.1-py3-none-any.whl", hash = "sha256:5923cafe8289c9e3c53cb6992f4b46ec3132473b9f9eb65eb33ad28e2682db81", size = 479527, upload-time = "2026-04-24T15:21:45.568Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/3a6082f7809bdcc1269e9920c07d7c7f92a53cc265a4a879e59c92b23b36/langfuse-4.6.1-py3-none-any.whl", hash = "sha256:a696ac3089a0c8431bf7f1b47b7f6417da311f418dd04ce9ef62d63608fd8797", size = 481237, upload-time = "2026-05-08T14:08:17.141Z" }, ] [[package]] name = "librt" -version = "0.9.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/4a/c64265d71b84030174ff3ac2cd16d8b664072afab8c41fccd8e2ee5a6f8d/librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443", size = 67529, upload-time = "2026-04-09T16:04:27.373Z" }, - { url = "https://files.pythonhosted.org/packages/23/b1/30ca0b3a8bdac209a00145c66cf42e5e7da2cc056ffc6ebc5c7b430ddd34/librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c", size = 70248, upload-time = "2026-04-09T16:04:28.758Z" }, - { url = "https://files.pythonhosted.org/packages/fa/fc/c6018dc181478d6ac5aa24a5846b8185101eb90894346db239eb3ea53209/librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e", size = 202184, upload-time = "2026-04-09T16:04:29.893Z" }, - { url = "https://files.pythonhosted.org/packages/bf/58/d69629f002203370ef41ea69ff71c49a2c618aec39b226ff49986ecd8623/librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285", size = 212926, upload-time = "2026-04-09T16:04:31.126Z" }, - { url = "https://files.pythonhosted.org/packages/cc/55/01d859f57824e42bd02465c77bec31fa5ef9d8c2bcee702ccf8ef1b9f508/librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2", size = 225664, upload-time = "2026-04-09T16:04:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/32f63ad0ef085a94a70315291efe1151a48b9947af12261882f8445b2a30/librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce", size = 219534, upload-time = "2026-04-09T16:04:33.667Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5a/9d77111a183c885acf3b3b6e4c00f5b5b07b5817028226499a55f1fedc59/librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f", size = 227322, upload-time = "2026-04-09T16:04:34.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/05d700c93063753e12ab230b972002a3f8f3b9c95d8a980c2f646c8b6963/librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236", size = 223407, upload-time = "2026-04-09T16:04:36.22Z" }, - { url = "https://files.pythonhosted.org/packages/c0/26/26c3124823c67c987456977c683da9a27cc874befc194ddcead5f9988425/librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38", size = 221302, upload-time = "2026-04-09T16:04:37.62Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/c7cc2be5cf4ff7b017d948a789256288cb33a517687ff1995e72a7eea79f/librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b", size = 243893, upload-time = "2026-04-09T16:04:38.909Z" }, - { url = "https://files.pythonhosted.org/packages/62/d3/da553d37417a337d12660450535d5fd51373caffbedf6962173c87867246/librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774", size = 55375, upload-time = "2026-04-09T16:04:40.148Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5a/46fa357bab8311b6442a83471591f2f9e5b15ecc1d2121a43725e0c529b8/librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8", size = 62581, upload-time = "2026-04-09T16:04:41.452Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, - { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, - { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, - { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, - { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, - { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, - { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, - { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, - { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, - { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, - { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, - { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, - { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, - { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, - { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, - { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, - { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, - { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, - { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, - { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, - { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, - { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, - { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, - { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, - { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, - { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, - { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, - { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, - { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, - { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, - { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, - { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, - { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, + { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, + { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, + { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, + { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, + { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, + { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, + { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, + { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, + { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, + { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, + { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, ] [[package]] name = "litellm" -version = "1.83.14" +version = "1.85.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3485,9 +3602,9 @@ dependencies = [ { name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/7c/c095649380adc96c8630273c1768c2ad1e74aa2ee1dd8dd05d218a60569f/litellm-1.83.14.tar.gz", hash = "sha256:24aef9b47cdc424c833e32f3727f411741c690832cd1fe4405e0077144fe09c9", size = 14836599, upload-time = "2026-04-26T03:16:10.176Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/d5/3c9b560db2ffa9e498655d0dfd74f408bc5b32ede858b5731c2a5fa4c752/litellm-1.85.0.tar.gz", hash = "sha256:babdd569809af913d08a08a7eb55df1ed3e6a3960ee365c6cef4ad031c9bc72a", size = 15344387, upload-time = "2026-05-17T01:59:15.97Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/5c/1b5691575420135e90578543b2bf219497caa33cfd0af64cb38f30288450/litellm-1.83.14-py3-none-any.whl", hash = "sha256:92b11ba2a32cf80707ddf388d18526696c7999a21b418c5e3b6eda1243d2cfdb", size = 16457054, upload-time = "2026-04-26T03:16:05.72Z" }, + { url = "https://files.pythonhosted.org/packages/1c/38/e6a4abb062e039d18d59538cc4e6fc370c2c10cd2bff4a2e546acb69dcb9/litellm-1.85.0-py3-none-any.whl", hash = "sha256:2bb449153610691faffd76f5b94a8c29e4b66fc5394156ebf54fd4fe92759b1a", size = 16978229, upload-time = "2026-05-17T01:59:11.902Z" }, ] [package.optional-dependencies] @@ -3522,20 +3639,20 @@ proxy = [ [[package]] name = "litellm-enterprise" -version = "0.1.39" +version = "0.1.40" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3d/0b/79fb68abf7c787d951dd367f662c52b922278548f244f5d36e623cdb2161/litellm_enterprise-0.1.39.tar.gz", hash = "sha256:434e2c15280218bb9224adbbac878bcffe0b8a75b0b46deeb0b90bc4f2e2152b", size = 69465, upload-time = "2026-04-26T03:09:36.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b5/c1ef8cbb4555564dc47489d65758d4132bb1a3a5c0991264c57e0069b059/litellm_enterprise-0.1.40.tar.gz", hash = "sha256:f2bc6d8ed3863f51d2aaafa99f2a5cda6ccf9fc64a9c646825ab8051a789bc62", size = 70107, upload-time = "2026-05-05T23:28:14.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b0/30df9b36366559efd9c1fae39c67856481c7418056eb2196266bda605bc8/litellm_enterprise-0.1.39-py3-none-any.whl", hash = "sha256:e5f48745fb127dc4f72fd1fa7cdeba0ddd4066dc5f0d9e8e87eea4e4571d42b3", size = 136645, upload-time = "2026-04-26T03:09:35.492Z" }, + { url = "https://files.pythonhosted.org/packages/37/45/af3e0922805f81262bdfc6002567e004205b144fd2d011c2891da6f48265/litellm_enterprise-0.1.40-py3-none-any.whl", hash = "sha256:bf0eada2309053556aef09d9071297f3c5952e787df9c283de13a393b6b85251", size = 137250, upload-time = "2026-05-05T23:28:13.82Z" }, ] [[package]] name = "litellm-proxy-extras" -version = "0.4.69" +version = "0.4.72" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/e8/0176368d64ffaaf7ff7da07a7833ef05cd92484cf21167a9291cb311568f/litellm_proxy_extras-0.4.69.tar.gz", hash = "sha256:8c24a01a4dffb137e95c709a47ab68053591ccdf7d78a038c57348f5b2ab990d", size = 41220, upload-time = "2026-04-26T03:12:12.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/fb/da50c6cb89901b316e8c29c5049bfd4f916ac04e5d49b2971183c066274f/litellm_proxy_extras-0.4.72.tar.gz", hash = "sha256:a9543165eb5e8b440a2a92963a67fcf143a15457bfdcb8fc05e68325aee75c0e", size = 43483, upload-time = "2026-05-14T05:42:17.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/58/165a96b061fa90824ffbce13191262d4a0089510284a973805e5854e2c03/litellm_proxy_extras-0.4.69-py3-none-any.whl", hash = "sha256:4aee8dab05d1a6f91ba89da729d241122eaad4cbe64f39b19ea6a855543146c4", size = 113230, upload-time = "2026-04-26T03:12:10.731Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e1/899e05e740695c0562040b62df897e6182b3e6f88ce719a3c53bffeebc1c/litellm_proxy_extras-0.4.72-py3-none-any.whl", hash = "sha256:869c40cb459e0c40c93d29eb4d4d8b3a197050c0673d7eca1765a034a690fb93", size = 117821, upload-time = "2026-05-14T05:42:16.412Z" }, ] [[package]] @@ -3553,14 +3670,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -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/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } 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/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -3659,7 +3776,7 @@ dependencies = [ { name = "fonttools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "kiwisolver", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyparsing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3725,7 +3842,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.27.0" +version = "1.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3743,9 +3860,9 @@ dependencies = [ { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/83/d1efe7c2980d8a3afa476f4e3d42d53dd54c0ab94c27bee5d755b45c8b73/mcp-1.27.1.tar.gz", hash = "sha256:0f47e1820f8f8f941466b39749eb1d1839a04caddca2bc60e9d46e8a99914924", size = 608458, upload-time = "2026-05-08T16:50:12.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/42d9596facebdb533b7f0b86c1b0364ef350d1f8ba78b1052e8a58b48b65/mcp-1.27.1-py3-none-any.whl", hash = "sha256:1af3c4203b329430fde7a87b4fcb6392a041f5cb851fd68fc674016ab4e7c06f", size = 216260, upload-time = "2026-05-08T16:50:10.547Z" }, ] [package.optional-dependencies] @@ -3826,7 +3943,7 @@ version = "0.5.4" 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' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ @@ -4124,11 +4241,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.20.0" +version = "2.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/f3/257adc69a71011b4c8cda321b00f02c5bf1980ae38ffd05a58d9632d4de8/narwhals-2.20.0.tar.gz", hash = "sha256:c10994975fa7dc5a68c2cffcddbd5908fc8ebb2d463c5bab085309c0ee1f551e", size = 627848, upload-time = "2026-04-20T12:11:45.427Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/a0/6198c56d42ef2f3c6ed0c42ba30dbcefdc86a91262d7d449010770ae085b/narwhals-2.21.2.tar.gz", hash = "sha256:5c5b2d0b47aef7c73ea412cfcbcd467f2f2d5be73e3c2ab19d78f4a97718790a", size = 632176, upload-time = "2026-05-16T08:49:08.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/69/f24d3d1c38ad69e256138b4ec2452a8c7cf66be49dc214771ae99dd4f0a0/narwhals-2.20.0-py3-none-any.whl", hash = "sha256:16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d", size = 449373, upload-time = "2026-04-20T12:11:43.596Z" }, + { url = "https://files.pythonhosted.org/packages/1d/77/928ea2e70641ca177a11140062cc5840d421795f2e82749d408d0cce900a/narwhals-2.21.2-py3-none-any.whl", hash = "sha256:7bb57c3700486039215455b9bf2d64261915cc0fd845cc30272d631df696b251", size = 451201, upload-time = "2026-05-16T08:49:05.536Z" }, ] [[package]] @@ -4209,7 +4326,7 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.4" +version = "2.4.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -4225,79 +4342,79 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' 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" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } 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" }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, - { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, - { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, - { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, - { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, - { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, - { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, - { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, - { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, - { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, - { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, - { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, - { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, - { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, - { 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" }, + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, ] [[package]] @@ -4324,7 +4441,7 @@ wheels = [ [[package]] name = "openai" -version = "2.24.0" +version = "2.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -4336,32 +4453,33 @@ dependencies = [ { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/50/5901f01ef14e6c27788beb91e54fef5d6204fb5fb9e97402fc8a14de2e32/openai-2.37.0.tar.gz", hash = "sha256:f4bc562cc5f3a43d40d678105572d9d44765f6e0f50c125f63055419b72f4bd9", size = 754706, upload-time = "2026-05-15T22:30:35.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/bce61680d0699a78a405fd9a67989b175ba020590428831aab2ab1d2be7c/openai-2.37.0-py3-none-any.whl", hash = "sha256:814633888b8f3b1ffd6615697c6e4ef93632d08b7c2e28c8c5ef3556e5a10107", size = 1303238, upload-time = "2026-05-15T22:30:32.767Z" }, ] [[package]] name = "openai-agents" -version = "0.10.5" +version = "0.17.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "griffelib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "mcp", extra = ["ws"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "websockets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/38/f47644c5de02f1853483d1a16d1fb7d12cc2c219c5548ec26a3e9aee1c29/openai_agents-0.10.5.tar.gz", hash = "sha256:73cef5263eeb98437b874b29c800694617af7d9626be19514b4ed6f434874c1e", size = 2511640, upload-time = "2026-03-05T20:43:59.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/16/b79c1849125eb6d19cae98c21ff35caa2e55b5ec8d7a02b354b711917ef7/openai_agents-0.17.3.tar.gz", hash = "sha256:63b6dda6bd4fb51169e2a2cbd5d187a4e5ce823bbd15f965c8ed1d3b89072eec", size = 5406135, upload-time = "2026-05-19T01:28:15.971Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/07/ad27018fb42d6f1e70f471a5ca3f6398a2159575b623edf86e1ddde66ce4/openai_agents-0.10.5-py3-none-any.whl", hash = "sha256:6c92491c61ba85b4790d76562b4af2e6e230c8844f9c12fed8a721400a320c86", size = 413046, upload-time = "2026-03-05T20:43:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/80/ec/775a14cfd5f12f4ffe458c7ac9527831093c72e8c1aef682898fc6394106/openai_agents-0.17.3-py3-none-any.whl", hash = "sha256:a048bb0752d40913d18bccf6562f56260b603bb57c972597b6da58f60123f4bd", size = 841541, upload-time = "2026-05-19T01:28:13.334Z" }, ] [[package]] name = "openai-chatkit" -version = "1.6.4" +version = "1.6.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -4370,9 +4488,9 @@ dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", extra = ["standard"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/4b/acd3155d535398656c829fb0fed2f71a7146114b05b6ed55351921f74196/openai_chatkit-1.6.4.tar.gz", hash = "sha256:68c7f6091987bec97bc8a9ad2e1c815d3f43c5abe642b0e2d3d653364478fa66", size = 64756, upload-time = "2026-05-14T21:23:55.192Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/07/c4b4ea034f34f25e73cf1a872deb349e3acab6c929a5531d547a9a994890/openai_chatkit-1.6.5.tar.gz", hash = "sha256:903e9702bf26cd8a2b23d4e7b199b657bee4379758e0ca11ebaee09362d2889e", size = 65057, upload-time = "2026-05-19T05:05:14.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/c3/b57f9a4991f3bfc4c1e4fad743822eab1929c246b60a87aded12d735e116/openai_chatkit-1.6.4-py3-none-any.whl", hash = "sha256:d20008ab5d2e837044d606171801623a21e5081150f2bcbeb8613a605186fb03", size = 44019, upload-time = "2026-05-14T21:23:53.718Z" }, + { url = "https://files.pythonhosted.org/packages/aa/00/64b0faae946885e9bf52a8ea77d8bd01c80f6d0be967a30c1d4e73fc2ce1/openai_chatkit-1.6.5-py3-none-any.whl", hash = "sha256:9e16d3bdc6c15fec900801591dc5bb8ba9446447d3db2247dc8120518f1ca3c0", size = 44094, upload-time = "2026-05-19T05:05:13.654Z" }, ] [[package]] @@ -4880,7 +4998,7 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.2" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -4897,59 +5015,59 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "python-dateutil", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "tzdata", marker = "python_full_version >= '3.11' and 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" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } 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" }, - { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, - { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, - { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, - { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, - { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, - { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, - { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, - { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, - { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, - { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, - { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, - { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, - { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, - { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, - { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, - { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, - { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, - { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, - { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, + { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, + { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, + { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, + { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, ] [[package]] @@ -5070,11 +5188,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1" +version = "26.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/7e/d2b04004e1068ad4fdfa2f227b839b5d03e602e47cdbbf49de71137c9546/pip-26.1.tar.gz", hash = "sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3", size = 1840316, upload-time = "2026-04-26T21:00:05.406Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/48/cb9b7a682f6fe01a4221e1728941dd4ac3cd9090a17db3779d6ff490b602/pip-26.1.1.tar.gz", hash = "sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78", size = 1840400, upload-time = "2026-05-04T19:02:21.248Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", hash = "sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", size = 1812804, upload-time = "2026-04-26T21:00:03.194Z" }, + { url = "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", hash = "sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", size = 1812777, upload-time = "2026-05-04T19:02:18.9Z" }, ] [[package]] @@ -5155,18 +5273,17 @@ wheels = [ [[package]] name = "posthog" -version = "7.13.1" +version = "7.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "distro", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, - { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/09/ecc82b5ba5876164a3807adcc5101466da1e4416600075bdbd2071327457/posthog-7.13.1.tar.gz", hash = "sha256:5e53c57db076807530bbec5634c96673ceae8e8e58b99c983af26f02bb4759aa", size = 194124, upload-time = "2026-04-24T19:08:32.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/98/e4f414fd390bba6242be37f0a899325abba5ab22e3e83213cbb20253f807/posthog-7.15.0.tar.gz", hash = "sha256:22270215d7b062cd66badee3dfbe957e3f05e29b1b0e8c125a214ebc50bc77c8", size = 212310, upload-time = "2026-05-19T12:57:03.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/bf/eafd5e7508b03264b7deb4db6563c4a2830de7114e01ccbf369756b779d1/posthog-7.13.1-py3-none-any.whl", hash = "sha256:fc0f4b4a8878957e1ea8d319b2e4038b66a19625837f59b020cddaaf59fce982", size = 228291, upload-time = "2026-04-24T19:08:30.822Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4a/7b50a53e9a557c7e78deb36811eb6b0d05f40f20c3b633e379e206426e37/posthog-7.15.0-py3-none-any.whl", hash = "sha256:0ada8fe94c7fb9b7ad507180eed308fea3b45063ddb4088cc8eaddb9cdea15c4", size = 248461, upload-time = "2026-05-19T12:57:01.575Z" }, ] [[package]] @@ -5217,128 +5334,142 @@ wheels = [ [[package]] name = "propcache" -version = "0.4.1" +version = "0.5.2" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } 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" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, + { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, + { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, + { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, + { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, + { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, + { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] name = "proto-plus" -version = "1.27.2" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, ] [[package]] @@ -5460,7 +5591,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5468,9 +5599,9 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [package.optional-dependencies] @@ -5492,134 +5623,132 @@ wheels = [ [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +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/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { 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]] name = "pydantic-settings" -version = "2.14.0" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] [[package]] @@ -5827,11 +5956,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.26" +version = "0.0.27" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, + { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, ] [[package]] @@ -5857,11 +5986,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.1.post1" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +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/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, + { 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]] @@ -5952,21 +6081,21 @@ wheels = [ [[package]] name = "qdrant-client" -version = "1.17.1" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "httpx", extra = ["http2"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "portalocker", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/dd/f8a8261b83946af3cd65943c93c4f83e044f01184e8525404989d22a81a5/qdrant_client-1.17.1.tar.gz", hash = "sha256:22f990bbd63485ed97ba551a4c498181fcb723f71dcab5d6e4e43fe1050a2bc0", size = 344979, upload-time = "2026-03-13T17:13:44.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" }, ] [[package]] @@ -5989,7 +6118,7 @@ dependencies = [ { name = "jsonpath-ng", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "ml-dtypes", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-ulid", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -6017,128 +6146,128 @@ wheels = [ [[package]] name = "regex" -version = "2026.4.4" +version = "2026.5.9" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } 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" }, - { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, - { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, - { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, - { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, - { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, - { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, - { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, - { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, - { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, - { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, - { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, - { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, - { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, - { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, - { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, - { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, - { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, - { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, - { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, - { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, - { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, - { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, - { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, - { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, - { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, - { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, - { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, - { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, - { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, - { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, - { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, - { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, - { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, + { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, + { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, + { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, + { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, + { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, + { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, ] [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -6146,9 +6275,9 @@ dependencies = [ { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -6350,14 +6479,14 @@ wheels = [ [[package]] name = "s3transfer" -version = "0.16.1" +version = "0.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/29/af14f4ef3c11a50435308660e2cc68761c9a7742475e0585cd4396b91777/s3transfer-0.16.1.tar.gz", hash = "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524", size = 154801, upload-time = "2026-04-22T20:36:06.475Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/19/90d7d4ed51932c022d53f1d02d564b62d10e272692a1f9b76425c1ad2a02/s3transfer-0.16.1-py3-none-any.whl", hash = "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", size = 86825, upload-time = "2026-04-22T20:36:04.992Z" }, + { url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" }, ] [[package]] @@ -6429,7 +6558,7 @@ resolution-markers = [ ] dependencies = [ { name = "joblib", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "threadpoolctl", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] @@ -6553,7 +6682,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] 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 = [ @@ -6626,9 +6755,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } wheels = [ @@ -6835,15 +6964,15 @@ wheels = [ [[package]] name = "sse-starlette" -version = "3.4.1" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/9a/f35932a8c0eb6b2287b66fa65a0321df8c84e4e355a659c1841a37c39fdb/sse_starlette-3.4.1.tar.gz", hash = "sha256:f780bebcf6c8997fe514e3bd8e8c648d8284976b391c8bed0bcb1f611632b555", size = 35127, upload-time = "2026-04-26T13:32:32.292Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/07/45c21ed03d708c477367305726b89919b020a3a2a01f72aaf5ad941caf35/sse_starlette-3.4.1-py3-none-any.whl", hash = "sha256:6b43cf21f1d574d582a6e1b0cfbde1c94dc86a32a701a7168c99c4475c6bd1d0", size = 16487, upload-time = "2026-04-26T13:32:30.819Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, ] [[package]] @@ -6908,7 +7037,7 @@ dependencies = [ { name = "loguru", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "matplotlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "plotly", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic-argparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -6956,93 +7085,90 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, - { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, - { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, + { url = "https://files.pythonhosted.org/packages/38/e3/03c90dadcf5b3f82b83cee9adee60ef666b329c654f58c066af44eae0287/tiktoken-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:47b1df8d73390a24f94980c75158cdd5c56d256f16d55f30cb49c230caba9ba4", size = 1036627, upload-time = "2026-05-15T04:50:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/760463e5b2e8ad2bc229ae0a17ecb06727b6cbc094f08d8f65844315632e/tiktoken-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d40c6c5aab171dcd6eb8455bc567bde404bb9def60cdb8c1299cc782b242bb9", size = 984699, upload-time = "2026-05-15T04:50:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/de/8a/8895f342a6b6aabd1a358e672f6f077b3ae51d0c63ca605d142db3bcd8ab/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:9b842981fa91accdffd48ff6408a977b7a91c3fbda55d353c3c68114d5c9d69e", size = 1118690, upload-time = "2026-05-15T04:50:14.234Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/92557768fb0801f0d9dd9243cb9b6d342900b05e4b1006d4771f49ce233e/tiktoken-0.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed5a30027cb4d8c7ca8b273d4766f3db3cf58fad9e9f3b1a68a351ffb54873d5", size = 1138423, upload-time = "2026-05-15T04:50:15.668Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b9/a3d99feeedb032ffd09cd6652077f86bdee9a70dd0b990b2b272b445d4c3/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7ab10f4a21c2999846940113f6dbd72e0fa06a24119feddd74cc47e85818e06d", size = 1185077, upload-time = "2026-05-15T04:50:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/cc/93/bab868277d475dc6d2aaacd34cdd239c282f4908dcc8702e0a3311a8e032/tiktoken-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a2937ad042d49d50eac6e1ba07c5661d4bd3942a5b1e0c0d08475c4df83676e1", size = 1241702, upload-time = "2026-05-15T04:50:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/27e9f7e0ed76e501cfefc9fb2112df4c7bf70ca96945b15ecb7615aac860/tiktoken-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:44733b99bfd72b590cd0936b1c01b3b4dd73122db2d544bc1ceeb18a7678c910", size = 876565, upload-time = "2026-05-15T04:50:20.268Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4c/1bc81f4cd53e827c4ee67ca951b5935724716049452d8dfa09b8b82372bb/tiktoken-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7bfe1849caa65d1e1d9871817170ec497bbb7984e182012e1bdce72f66608cdb", size = 1036353, upload-time = "2026-05-15T04:50:21.757Z" }, + { url = "https://files.pythonhosted.org/packages/75/91/10b9c7076bc02c246c853201fdbbe300a4b8c5ed7b84c25f7403f4e32655/tiktoken-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:91c180fe255bd5a86d8316210d2833a1d4d33d026cd86a67812f4773743c8d26", size = 984644, upload-time = "2026-05-15T04:50:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e4/fceae98015fab47fcd49b8bd7f46145bcd187a47e0add1e5378ed67ef980/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:059c8ecf554eb5b41e6e054ba467b871b03277d267dee7244380aca4359747d4", size = 1119261, upload-time = "2026-05-15T04:50:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/fe42ad00de01a8c4a49ad8649a2c8a316835a9cad5961b11d21eac0020a5/tiktoken-0.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36217497eaffc158607a3b26f065300db2aefd43b115263f3b9688ce38146173", size = 1138253, upload-time = "2026-05-15T04:50:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/ccee1ecccca107e9a16efcecdeeb964c325305038554d466ece65b42338f/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:303f7d91b4fce3baddbcde05c139091d4caa5026ac7214c1dc7ff7a71ee429ff", size = 1185747, upload-time = "2026-05-15T04:50:27.02Z" }, + { url = "https://files.pythonhosted.org/packages/9d/03/cd0cba295522b91eb55c6b2704f1df895f8226cfe60ab10d4d51d0cc9e69/tiktoken-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5d48843bee149630eb735a99e1f4a85b47308d21868ea63163f6e87768d3cfed", size = 1241265, upload-time = "2026-05-15T04:50:28.815Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/a10efd564402d82c2ff50d12057353ace447aa8007deceaa48641f63d35c/tiktoken-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc1c44cd37b43fc46bae593129164f4f281e82ea116b57a85aa81bda57eafc94", size = 876509, upload-time = "2026-05-15T04:50:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, ] [[package]] name = "tokenizers" -version = "0.22.2" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, - { 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" }, + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, ] [[package]] @@ -7131,7 +7257,7 @@ wheels = [ [[package]] name = "typer" -version = "0.23.1" +version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -7139,9 +7265,9 @@ dependencies = [ { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "shellingham", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, ] [[package]] @@ -7164,14 +7290,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.33.0.20260408" +version = "2.33.0.20260518" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/6a/749dc53a54a3f35842c1f8197b3ca6b54af6d7458a1bfc75f6629b6da666/types_requests-2.33.0.20260408.tar.gz", hash = "sha256:95b9a86376807a216b2fb412b47617b202091c3ea7c078f47cc358d5528ccb7b", size = 23882, upload-time = "2026-04-08T04:34:49.33Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/b8/78fd6c037de4788c040fdd323b3369804400351b7827473920f6c1d03c10/types_requests-2.33.0.20260408-py3-none-any.whl", hash = "sha256:81f31d5ea4acb39f03be7bc8bed569ba6d5a9c5d97e89f45ac43d819b68ca50f", size = 20739, upload-time = "2026-04-08T04:34:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, ] [[package]] @@ -7218,11 +7344,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -7253,16 +7379,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.46.0" +version = "0.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, ] [package.optional-dependencies] @@ -7342,105 +7468,119 @@ wheels = [ [[package]] name = "watchfiles" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5a/2bf22ecb24916983bf1cc0095e7dea2741d14d6553b0d6a2ac8bc96eca93/watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9", size = 400471, upload-time = "2026-05-18T04:31:08.908Z" }, + { url = "https://files.pythonhosted.org/packages/55/70/dea1f6a0e76607841a60fb51af150e70124864673f61704abb62b90cdcc7/watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4", size = 394599, upload-time = "2026-05-18T04:30:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/18/52/752dcc7dc817baef5e89518732925795ce52e36a683a9a3c9fb68b21504e/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631", size = 455458, upload-time = "2026-05-18T04:30:29.126Z" }, + { url = "https://files.pythonhosted.org/packages/12/48/366ebbb22fcc504c2f72b45f0b7e72f40a18795cc01752c16066d597b67a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994", size = 460513, upload-time = "2026-05-18T04:31:40.85Z" }, + { url = "https://files.pythonhosted.org/packages/ad/44/1f9e1b15e7a729062e0d0c3d0d7225ea4ab98b2267ef87287153be2495fc/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e", size = 493616, upload-time = "2026-05-18T04:30:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/7e/55/8b1086dcc8a1d6a697a62767bd7ea368e74c61c6fd171683cfe24a3fe5d2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19", size = 573154, upload-time = "2026-05-18T04:30:37.903Z" }, + { url = "https://files.pythonhosted.org/packages/14/7a/242f400cc77fafa7b18d53d19d9cb64fc6a6f61f28c55913bae7c674d92a/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8", size = 467046, upload-time = "2026-05-18T04:30:41.869Z" }, + { url = "https://files.pythonhosted.org/packages/02/c8/79eee650c62d2c186598489814468e389b5def0ebe755399ff645b35b1b2/watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07", size = 457100, upload-time = "2026-05-18T04:31:13.064Z" }, + { url = "https://files.pythonhosted.org/packages/81/36/519f6dbb7a95e4fe7c1513ed25b1520295ef9905a27f1f2226a73892bfb7/watchfiles-1.2.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551", size = 467038, upload-time = "2026-05-18T04:30:32.915Z" }, + { url = "https://files.pythonhosted.org/packages/2f/12/951af6b9f89097e02511122258402cb3578443021930b70cf968d6310dc0/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310", size = 632563, upload-time = "2026-05-18T04:30:11.539Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/0cba1f0a6117b7ec117271bdc3cb3a5a252005959755a2c09a745e0942cc/watchfiles-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df", size = 660851, upload-time = "2026-05-18T04:31:53.186Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/26347558cc8bf6877845e66b315f644d03c173906aa09e233a3f4fd23928/watchfiles-1.2.0-cp310-cp310-win32.whl", hash = "sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1", size = 277023, upload-time = "2026-05-18T04:30:18.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/68/a5e67b6b68e94f4c1511d61c46c55eba0737583620b6febf194c7b9cc23f/watchfiles-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d", size = 290107, upload-time = "2026-05-18T04:32:09.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, ] [[package]] @@ -7606,149 +7746,125 @@ wheels = [ [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "multidict", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "propcache", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } 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" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, + { url = "https://files.pythonhosted.org/packages/3f/df/f1c7a3de0831cd83194f1a85c5bb431b13f81e6b45079314c86d1c4ef3f2/yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12", size = 129057, upload-time = "2026-05-19T21:27:47.564Z" }, + { url = "https://files.pythonhosted.org/packages/48/41/7daafb32dd7562bf45b1ce56562e7e1a9146f6479b6456873eb8a3413c40/yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0", size = 91545, upload-time = "2026-05-19T21:27:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8f/7b3ec212f1ea0683f55f978e3246bc313c38818664edfc97a9f349a4901e/yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75", size = 91380, upload-time = "2026-05-19T21:27:51.953Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1b/8bafab7db23b0567ae9db749099b329d91e3b82bc6028b2050ba583e116c/yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727", size = 105957, upload-time = "2026-05-19T21:27:53.98Z" }, + { url = "https://files.pythonhosted.org/packages/7f/77/21030c2f8d21d21559719beafc772ada2014be933418ed1eaed9cc800e42/yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413", size = 97242, upload-time = "2026-05-19T21:27:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/f9ea63d1b6aa910a866e089d871fff6cbd49caab29b86b35221a62dfa0d5/yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9", size = 114719, upload-time = "2026-05-19T21:27:58.037Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/04e0ee98ac58a249ea7ed75223f5f901ba81a834f0b4921b58e5cec11757/yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2", size = 112140, upload-time = "2026-05-19T21:27:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/ad/0b9cc9f38a7324a7eb1d80f834eaa5283d17e9271bbda3186e598dddaeac/yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90", size = 106721, upload-time = "2026-05-19T21:28:02.586Z" }, + { url = "https://files.pythonhosted.org/packages/65/e7/a52478ebfc66ec989e085c6ae038b9f1bfa4190baa193b133b669c709e2f/yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643", size = 106478, upload-time = "2026-05-19T21:28:04.523Z" }, + { url = "https://files.pythonhosted.org/packages/04/d8/5508530fea8472542de00013ae280765fc938ee196fc4030c43a498afb36/yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac", size = 105423, upload-time = "2026-05-19T21:28:06.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/f1/ece28505e9628e8b756e11bb4f28864a17cc33b6b44db4d2aaf0622bf630/yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f", size = 99878, upload-time = "2026-05-19T21:28:08.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/52/fb5d34529b46dd84013afcfb30b8d2bc2832ed03d412736f577d604fa393/yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36", size = 114025, upload-time = "2026-05-19T21:28:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/ff9d31aaab024f7a251c0ed308a98ae29bf9f7dc344e78f28b1322431ca2/yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a", size = 105613, upload-time = "2026-05-19T21:28:12.784Z" }, + { url = "https://files.pythonhosted.org/packages/31/7d/3296fb3f3ecd52bf9ae6c16b0895c1cda7e9170a2083861552b683f70264/yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53", size = 111665, upload-time = "2026-05-19T21:28:14.393Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/77aa6ddaca4fbf42e45e675a465c43956dd40702281049975a2aa04eae59/yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342", size = 106914, upload-time = "2026-05-19T21:28:15.893Z" }, + { url = "https://files.pythonhosted.org/packages/d8/02/7611f22cd1d4ed7373eb7f9ee21fde1046edba2e7c0e514880d760352f48/yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4", size = 92658, upload-time = "2026-05-19T21:28:17.471Z" }, + { url = "https://files.pythonhosted.org/packages/91/00/671d0add79938127292839ae44506ce2f7fe8909c72d5a931864f128fd0b/yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39", size = 87887, upload-time = "2026-05-19T21:28:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/1ce244152ff2839645e7cae92f90e7bafcb2c52bea7ff586ac714f14f5df/yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1", size = 128971, upload-time = "2026-05-19T21:28:20.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/5a/00f36967203ed89cb3acd2c8ed526cc3fed9418eb70ce128160a911c8499/yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c", size = 91507, upload-time = "2026-05-19T21:28:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/31/d0/1fb0c1cd27288f39f6974da4318c32768d72c9890984541fdf1e2e32a51d/yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d", size = 91343, upload-time = "2026-05-19T21:28:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/03/ce/d4a646508bed2f8dec6435b40166fe9308dd191262033d3f307b2bbcaecd/yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae", size = 105704, upload-time = "2026-05-19T21:28:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/b3278e82d8bc41485bcf6d856cd0433262593de615b1d3dc43bd3f5bead4/yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a", size = 97281, upload-time = "2026-05-19T21:28:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/17/5b/4cee6e7c92e487bebe7afc797da0aa54a248ab4e776a68fe369ec29665a5/yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e", size = 114020, upload-time = "2026-05-19T21:28:29.458Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/111076571545a7d4f9cca3fbd5c6f40615af58642be09f12328f48022468/yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50", size = 111450, upload-time = "2026-05-19T21:28:31.262Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ec/08f671f69a444d704aeecebf92af659b67b97a869942411d0a578b08c334/yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003", size = 106384, upload-time = "2026-05-19T21:28:32.856Z" }, + { url = "https://files.pythonhosted.org/packages/e5/86/ce41e7a7a199340b2330d52b60f25c4074b6636dd0e60b1a80d31a9db042/yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f", size = 106153, upload-time = "2026-05-19T21:28:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5d/31be8a729531ab3e55ac3e7e5c800be8c89ea98947f418b2f6ea259fb6ee/yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f", size = 105322, upload-time = "2026-05-19T21:28:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/47/9b/b57afb22b386ae87ac9940f09878b98d8c333f89113e6fc96fcf4ca9eb64/yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294", size = 99057, upload-time = "2026-05-19T21:28:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/06348c27c8389256c313e8a57d796808fc0264c915dd5e7cfd3c0e314dc7/yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2", size = 113502, upload-time = "2026-05-19T21:28:40.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1c/284f307b298e4a17b7943b07d9d7ecc4151537f8d137ba51f3bb6c31ca20/yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c", size = 105253, upload-time = "2026-05-19T21:28:41.987Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/0de123bec8619e45c80cbded9085f61b5b4a9eddb8abe6d25d28ee1ec866/yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b", size = 111345, upload-time = "2026-05-19T21:28:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/90/af/0248eb065e51129d2a9b2436cd1b5c772c19a6b04e5b6a186955671e3319/yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5", size = 106558, upload-time = "2026-05-19T21:28:45.806Z" }, + { url = "https://files.pythonhosted.org/packages/21/3c/f960d7a65ef97d8ba9b424fb5128796a4bc710fc6df2ddbbd7dfdc3bbd20/yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45", size = 92808, upload-time = "2026-05-19T21:28:48.465Z" }, + { url = "https://files.pythonhosted.org/packages/03/1a/49fb03750e4de4d2284cd5b885a383133c34eef45bd59631b2bb8b7e81e8/yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122", size = 87610, upload-time = "2026-05-19T21:28:50.07Z" }, + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] [[package]] name = "zipp" -version = "3.23.1" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] From 4609535e2291056885f64666eb3ead2465a61c5c Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Wed, 20 May 2026 02:35:23 +0200 Subject: [PATCH 10/22] Python: feat: add agent-framework-monty (Monty-backed CodeAct provider) (#5915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Python: feat: add agent-framework-monty (Monty-backed CodeAct) New alpha package that wraps pydantic-monty (a Rust-based Python interpreter) behind the same CodeAct API surface as agent-framework-hyperlight, so users can swap providers with minimal code change. Public API (agent_framework_monty): - MontyCodeActProvider — ContextProvider that injects a run-scoped execute_code tool plus dynamic CodeAct instructions. - MontyExecuteCodeTool — standalone FunctionTool for mixed-tool agents or manual static wiring. - FileMount / FileMountInput / MountMode — public types mirroring the Hyperlight names, with Monty's mode (read-only/read-write/overlay) and write_bytes_limit on FileMount. Constructor kwargs (both classes) mirror Hyperlight where possible: tools, approval_mode, workspace_root, file_mounts; plus a Monty-only resource_limits forwarding ResourceLimits to Monty.start(). Filesystem flow: - workspace_root auto-mounts at /input (read-write), matching Hyperlight. - file_mounts accepts string shorthand, (host, mount) tuple, or FileMount with mode + write cap. - Files written under read-write mounts are scanned post-execution and returned as Content.from_data items (mirrors Hyperlight /output). - overlay mounts buffer writes in-memory; read-only mounts reject writes. Internals: - _monty_bridge.InlineCodeBridge ports the inline (non-durable) bridge from anthonychu/maf-codeact-monty-python; handles FunctionSnapshot / FutureSnapshot pause/resume, dispatches direct typed calls + the call_tool fallback, forwards mount/limits to Monty.start(...). - generate_type_stubs emits per-tool stubs so Monty's `ty` type-checker rejects bad calls before any host tool runs. Alpha-policy compliance (per python-package-management skill): - Added agent-framework-monty = { workspace = true } to root pyproject.toml. - Added row to python/PACKAGE_STATUS.md. - Added monty entry under Experimental in python/AGENTS.md. - NOT added to core[all]; NO agent_framework.monty lazy shim (deferred to beta promotion). Samples (three sets, import from agent_framework_monty directly): - samples/02-agents/context_providers/code_act/monty_code_act.py (provider pattern) + updated local README. - samples/02-agents/tools/monty_code_interpreter/ (standalone + manual-wiring + README). - samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/ (full hosted-agent layout with uv-based pyproject.toml + Dockerfile, Azure Monitor wiring via APPLICATIONINSIGHTS_CONNECTION_STRING + enable_instrumentation, ENABLE_INSTRUMENTATION and ENABLE_SENSITIVE_DATA env vars). The alpha wheel is vendored into ./wheels/ (gitignored) via vendor-wheel.sh; new row added to the parent Responses-API README. Tests: - 28 hermetic unit tests (stubbed pydantic_monty). - 18 integration tests marked @pytest.mark.integration, auto-skipped when pydantic_monty is unimportable; exercise the real Monty runtime: print round-trip, last-expression value, direct typed tool dispatch, call_tool fallback, async tool, asyncio.gather parallelism, ty type-check rejection, OS blocked by default, workspace_root read+write capture, read-only / overlay mount semantics, resource_limits.max_duration_secs abort, approval gating end-to-end, full Agent run with a scripted chat client. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix: monty FileMount test compares against the normalized POSIX path The shorthand string mount goes through _normalize_mount_path, which rewrites Windows drive letters like 'C:\\Users\\...' into '/C:/Users/...' (POSIX-style). The Windows CI runners surfaced this because tmp_path resolves to a backslashed Windows path; the test was comparing against the raw str(host_a) instead of the normalized form. Compare against _normalize_mount_path(str(host_a)) so the assertion is platform-independent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix: address PR #5915 review feedback - _execute_code_tool docstring: clarify that the Monty backend supports scoped filesystem access via workspace_root / file_mounts (blocked by default). - _to_monty_mount: import pydantic_monty lazily through load_monty so missing-dependency errors surface as the same actionable RuntimeError the rest of the package raises (not a bare ImportError at module load). Renamed _load_monty -> load_monty for the same reason. - _python_type_repr: emit None for type(None) instead of Any, and normalize both typing.Union[...] and PEP-604 X | Y to PEP-604 syntax so Optional[X] / Union[..., None] / -> None signatures round-trip correctly through ty validation. Added a regression test. - _PrintCollector: track a running character count instead of recomputing sum(len(c) for c in self.chunks) per callback. Eliminates the O(n^2) cost on print-heavy code. - Instructions: mention that the value of the final expression is also returned alongside captured stdout (matches actual behavior). - 11_monty_codeact Dockerfile: pin ghcr.io/astral-sh/uv to 0.11.6 instead of :latest for reproducible builds. - 11_monty_codeact README: replace the bare "see parent README" pointer with sample-specific steps (./vendor-wheel.sh + uv sync + uv run), since the sample uses pyproject.toml + a vendored wheel rather than requirements.txt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: sample: 11_monty_codeact installs agent-framework-monty from PyPI Drop the vendored-wheel scaffolding now that agent-framework-monty is on PyPI as an alpha (1.0.0a*) release: - pyproject.toml: remove [tool.uv.sources] override; keep [tool.uv] prerelease = "allow" so uv pulls the alpha automatically. - Dockerfile: drop the COPY wheels/ step. - README: drop the ./vendor-wheel.sh setup step and the not-yet-on-PyPI warning. - Delete vendor-wheel.sh and the gitignored wheels/ directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix(monty): harden post-execution file capture against symlink escape Same class of issue as the MSRC-reported Hyperlight finding: the post-execution capture walked workspace_root with Path.rglob() + is_file() + read_bytes() - all of which follow symlinks. An attacker who controls the workspace (cloned repo, extracted archive, shared workspace) could pre-place `workspace/leak.txt -> /etc/passwd` or `workspace/outside_dir -> /etc/` and have host files surface as captured Content items. Monty's mount layer already rejects symlink reads from inside the sandbox across all three modes (verified empirically), so the runtime path was safe. This commit closes the post-execution scan path. Changes: - New `_iter_real_files(root)` walker that uses iterdir() + is_symlink() to skip symlinks at every directory level and yields only real files. Replaces the previous `host_root.rglob("*")` calls in both `_snapshot_writable_mounts` and `_capture_written_files`. - Use `Path.lstat()` instead of `Path.stat()` so size/mtime can never be taken from a symlink target. - Three new integration tests reproducing the MSRC attack shape against the workspace_root flow: symlink-to-file outside workspace, symlink-to-directory outside workspace, and a guard ensuring legitimate sandbox writes are still captured when symlinks are present. Per user request, hyperlight is untouched in this commit (separate fix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix(monty): skip symlink regression tests when unsupported Apply the same Windows-CI safety guard as the hyperlight fix in PR #5919: the three symlink integration tests create symlinks via Path.symlink_to(), which fails with OSError / NotImplementedError on unprivileged Windows runners. Add a local _symlinks_supported helper (mirroring the one in packages/core/tests/core/test_skills.py) and pytest.skip when symlinks aren't available, so the tests no longer fail for environment reasons. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: fix(monty): address PR #5915 follow-up review feedback - _invoke_tool: drop the inspect.iscoroutinefunction(...) branch and always `await self.tool_map[name](**kwargs)`. Every entry in tool_map is `partial(FunctionTool.invoke, skip_parsing=True)` and FunctionTool.invoke is `async def`, so the branching was dead code - and on Python versions affected by cpython#98590, iscoroutinefunction(partial(bound_async_method, ...)) returns False, causing the bridge to take the asyncio.to_thread path, return an unawaited coroutine, and surface it as a JSON-serialization failure for every tool call. Added a regression test test_invoke_tool_awaits_partial_wrapped_async_method. - generate_type_stubs: skip tools whose name is not a valid Python identifier or is a Python keyword. FunctionTool.name has no upstream validation, so a name like "weird-name" produced a syntax error in the stubs and a name like "broken\n pass\nasync def injected" would inject arbitrary stub source. Non-identifier names stay reachable via `call_tool("weird-name", ...)` at runtime; they just don't get type-checked stubs. Added regression test test_generate_type_stubs_skips_non_identifier_tool_names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/AGENTS.md | 1 + python/PACKAGE_STATUS.md | 1 + python/packages/monty/AGENTS.md | 78 +++ python/packages/monty/LICENSE | 21 + python/packages/monty/README.md | 179 +++++ .../monty/agent_framework_monty/__init__.py | 23 + .../_execute_code_tool.py | 558 +++++++++++++++ .../agent_framework_monty/_instructions.py | 125 ++++ .../agent_framework_monty/_monty_bridge.py | 327 +++++++++ .../monty/agent_framework_monty/_provider.py | 95 +++ .../monty/agent_framework_monty/_types.py | 38 ++ .../monty/agent_framework_monty/py.typed | 0 python/packages/monty/pyproject.toml | 107 +++ .../monty/tests/monty/test_monty_codeact.py | 642 ++++++++++++++++++ .../monty/test_monty_codeact_integration.py | 601 ++++++++++++++++ python/pyproject.toml | 1 + .../context_providers/code_act/README.md | 30 +- .../code_act/monty_code_act.py | 201 ++++++ .../tools/monty_code_interpreter/README.md | 40 ++ .../monty_code_interpreter.py | 114 ++++ .../monty_code_interpreter_manual_wiring.py | 136 ++++ .../foundry-hosted-agents/README.md | 3 +- .../responses/11_monty_codeact/Dockerfile | 25 + .../responses/11_monty_codeact/README.md | 116 ++++ .../11_monty_codeact/agent.manifest.yaml | 28 + .../responses/11_monty_codeact/agent.yaml | 15 + .../responses/11_monty_codeact/main.py | 136 ++++ .../responses/11_monty_codeact/pyproject.toml | 20 + python/uv.lock | 87 +++ 29 files changed, 3738 insertions(+), 10 deletions(-) create mode 100644 python/packages/monty/AGENTS.md create mode 100644 python/packages/monty/LICENSE create mode 100644 python/packages/monty/README.md create mode 100644 python/packages/monty/agent_framework_monty/__init__.py create mode 100644 python/packages/monty/agent_framework_monty/_execute_code_tool.py create mode 100644 python/packages/monty/agent_framework_monty/_instructions.py create mode 100644 python/packages/monty/agent_framework_monty/_monty_bridge.py create mode 100644 python/packages/monty/agent_framework_monty/_provider.py create mode 100644 python/packages/monty/agent_framework_monty/_types.py create mode 100644 python/packages/monty/agent_framework_monty/py.typed create mode 100644 python/packages/monty/pyproject.toml create mode 100644 python/packages/monty/tests/monty/test_monty_codeact.py create mode 100644 python/packages/monty/tests/monty/test_monty_codeact_integration.py create mode 100644 python/samples/02-agents/context_providers/code_act/monty_code_act.py create mode 100644 python/samples/02-agents/tools/monty_code_interpreter/README.md create mode 100644 python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter.py create mode 100644 python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter_manual_wiring.py create mode 100644 python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/Dockerfile create mode 100644 python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/README.md create mode 100644 python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.manifest.yaml create mode 100644 python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.yaml create mode 100644 python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/main.py create mode 100644 python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/pyproject.toml diff --git a/python/AGENTS.md b/python/AGENTS.md index f910919f95..80173560f7 100644 --- a/python/AGENTS.md +++ b/python/AGENTS.md @@ -93,3 +93,4 @@ python/ ### Experimental - [lab](packages/lab/AGENTS.md) - Experimental features +- [monty](packages/monty/AGENTS.md) - Monty-backed CodeAct integrations (alpha) diff --git a/python/PACKAGE_STATUS.md b/python/PACKAGE_STATUS.md index 2b9730a890..1f336f1cd8 100644 --- a/python/PACKAGE_STATUS.md +++ b/python/PACKAGE_STATUS.md @@ -37,6 +37,7 @@ Status is grouped into these buckets: | `agent-framework-hyperlight` | `python/packages/hyperlight` | `beta` | | `agent-framework-lab` | `python/packages/lab` | `beta` | | `agent-framework-mem0` | `python/packages/mem0` | `beta` | +| `agent-framework-monty` | `python/packages/monty` | `alpha` | | `agent-framework-ollama` | `python/packages/ollama` | `beta` | | `agent-framework-openai` | `python/packages/openai` | `released` | | `agent-framework-orchestrations` | `python/packages/orchestrations` | `beta` | diff --git a/python/packages/monty/AGENTS.md b/python/packages/monty/AGENTS.md new file mode 100644 index 0000000000..b8466af8c1 --- /dev/null +++ b/python/packages/monty/AGENTS.md @@ -0,0 +1,78 @@ +# Monty Package (agent-framework-monty) + +Monty-backed CodeAct integrations for the Microsoft Agent Framework. + +> [!NOTE] +> **Alpha package.** Not part of `agent-framework[all]` yet. Install explicitly +> with `pip install agent-framework-monty --pre`. + +## Core Classes + +- **`MontyCodeActProvider`** — `ContextProvider` that injects a run-scoped + `execute_code` tool plus dynamic CodeAct instructions. Mirrors the + `HyperlightCodeActProvider` API for the parts that apply to a non-sandboxed + Python interpreter. +- **`MontyExecuteCodeTool`** — `FunctionTool` that wraps the Monty interpreter. + Use directly for mixed-tool agents or manual static wiring. Mirrors + `HyperlightExecuteCodeTool`. + +## Public API + +```python +from agent_framework_monty import ( + FileMount, + FileMountInput, + MontyCodeActProvider, + MontyExecuteCodeTool, + MountMode, +) +``` + +`MontyCodeActProvider` and `MontyExecuteCodeTool` both accept: +- `tools` — host tool callables / `FunctionTool`s +- `approval_mode` — `"never_require"` (default) or `"always_require"` +- `workspace_root` — host directory auto-mounted at `/input` + (mirrors `HyperlightCodeActProvider.workspace_root`) +- `file_mounts` — sequence of `FileMountInput` (str shorthand, + `(host_path, mount_path)` tuple, or `FileMount`) +- `resource_limits` — Monty `ResourceLimits` TypedDict + +Tool-management methods on both classes: `add_tools`, `get_tools`, +`remove_tool`, `clear_tools`. Mount-management methods: `add_file_mounts`, +`get_file_mounts`, `remove_file_mount`, `clear_file_mounts`. + +`MontyExecuteCodeTool` additionally exposes: +- `build_instructions(*, tools_visible_to_model: bool) -> str` +- `create_run_tool() -> MontyExecuteCodeTool` +- `build_serializable_state() -> dict[str, Any]` +- `workspace_root`, `resource_limits` properties + +## Architecture + +- **`_types.py`** — `FileMount`, `FileMountInput`, `MountMode` (public). +- **`_provider.py`** — `MontyCodeActProvider` (thin wrapper around the tool). +- **`_execute_code_tool.py`** — `MontyExecuteCodeTool` plus tool / mount + normalization, approval helpers, dynamic `description`/`instructions` + builders, and the post-execution file-capture flow that surfaces files + written to `read-write` mounts as `Content.from_data` items. +- **`_monty_bridge.py`** — `InlineCodeBridge` and `generate_type_stubs`, + adapted from the reference Monty CodeAct repo. Pauses on `FunctionSnapshot` + to dispatch host calls, then resumes; supports direct typed tool calls, + the `call_tool` fallback, `asyncio.gather` fan-out, and forwards + ``mount`` / ``limits`` to `Monty(...).start(...)`. +- **`_instructions.py`** — dynamic instruction / tool-description builders + (include filesystem capability summaries when mounts are configured). + +## Not implemented (yet) + +| Capability | Monty primitive | Status | +|------------|-----------------|--------| +| Custom virtual filesystem | `OSAccess` subclass passed to `Monty(...).start(os=...)` | Not exposed. Strictly more general than file mounts; useful when you want a fully synthetic FS. | +| Outbound URL allow-list | No Monty primitive — expose `fetch_url` as a host tool with the allow-list check in your tool function. | Not exposed in this package; users add it as a regular tool. | + +## Out of scope (for now) + +- **Durable execution** — the reference Monty CodeAct repo also offers a + Durable-Functions-backed mode (`DurableCodeBridge`, `register_durable_codeact`, + `wait_for_external_event`, per-tool approval via external events). That is + intentionally not in this package yet. diff --git a/python/packages/monty/LICENSE b/python/packages/monty/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/monty/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/monty/README.md b/python/packages/monty/README.md new file mode 100644 index 0000000000..83d1ae79d1 --- /dev/null +++ b/python/packages/monty/README.md @@ -0,0 +1,179 @@ +# agent-framework-monty + +Monty-backed CodeAct integrations for Microsoft Agent Framework. + +> [!WARNING] +> This package is in **alpha**. APIs may change without notice. It is not part of +> `agent-framework[all]` yet; install it explicitly with `--pre`. + +## Installation + +```bash +pip install agent-framework-monty --pre +``` + +The package depends on [`pydantic-monty`](https://github.com/pydantic/monty), a +Rust-based Python interpreter, so it runs on Linux, macOS, and Windows wherever +Monty wheels are published — no hypervisor or WASM backend required. + +## Quick start + +### Context provider (recommended) + +Use `MontyCodeActProvider` to automatically inject the `execute_code` tool and +CodeAct instructions into every agent run. Tools registered on the provider are +available inside the Monty interpreter as **typed async functions** (e.g. +`await compute(operation="add", a=1, b=2)`), and as a fallback through +`call_tool(...)`. + +```python +from agent_framework import Agent, tool +from agent_framework_monty import MontyCodeActProvider + + +@tool +def compute(operation: str, a: float, b: float) -> float: + """Perform a math operation.""" + ops = {"add": a + b, "subtract": a - b, "multiply": a * b, "divide": a / b} + return ops[operation] + + +codeact = MontyCodeActProvider( + tools=[compute], + approval_mode="never_require", +) + +agent = Agent( + client=client, + name="CodeActAgent", + instructions="You are a helpful assistant.", + context_providers=[codeact], +) + +result = await agent.run("Multiply 6 by 7 using execute_code.") +``` + +### Standalone tool + +Use `MontyExecuteCodeTool` directly when you want full control over how the +tool is added to the agent (e.g. when mixing sandbox tools with direct-only +tools on the same agent). + +```python +from agent_framework import Agent, tool +from agent_framework_monty import MontyExecuteCodeTool + + +@tool +def send_email(to: str, subject: str, body: str) -> str: + """Send an email (direct-only, not available inside the sandbox).""" + return f"Email sent to {to}" + + +execute_code = MontyExecuteCodeTool( + tools=[compute], + approval_mode="never_require", +) + +agent = Agent( + client=client, + name="MixedToolsAgent", + instructions="You are a helpful assistant.", + tools=[send_email, execute_code], +) +``` + +### Manual static wiring + +For fixed configurations where provider lifecycle overhead is unnecessary, +build the CodeAct instructions once and pass them to the agent at construction +time: + +```python +execute_code = MontyExecuteCodeTool( + tools=[compute], + approval_mode="never_require", +) + +codeact_instructions = execute_code.build_instructions(tools_visible_to_model=False) + +agent = Agent( + client=client, + name="StaticWiringAgent", + instructions=f"You are a helpful assistant.\n\n{codeact_instructions}", + tools=[execute_code], +) +``` + +### File mounts and resource limits + +Mount host directories into the sandbox and cap execution resources: + +```python +from agent_framework_monty import FileMount, MontyCodeActProvider + +codeact = MontyCodeActProvider( + tools=[compute], + workspace_root="/host/workspace", # auto-mounted at /input (read-write) + file_mounts=[ + "/host/data", # shorthand: same path on both sides + ("/host/models", "/sandbox/models"), # explicit (host, mount_path) + FileMount( # full control + host_path="/host/cache", + mount_path="/sandbox/cache", + mode="overlay", # "read-only" | "read-write" | "overlay" + write_bytes_limit=10 * 1024 * 1024, + ), + ], + resource_limits={ # Monty ResourceLimits TypedDict + "max_duration_secs": 5.0, + "max_memory": 64 * 1024 * 1024, + }, +) +``` + +- **`workspace_root`** mirrors the Hyperlight default: the directory is mounted + at `/input` in `read-write` mode. +- **`file_mounts`** accepts a string shorthand, a `(host_path, mount_path)` + tuple, or a `FileMount` named tuple (with optional `mode` and + `write_bytes_limit`). +- Files written by the sandbox to any **`read-write`** mount are scanned + after each `execute_code` call and returned as `Content.from_data(...)` + attachments (with a `path` annotation in `additional_properties`), + mirroring Hyperlight's `/output` flow. +- `overlay` mounts buffer writes in memory (nothing leaks to the host and + nothing is captured). `read-only` mounts reject writes. +- **`resource_limits`** is forwarded straight to Monty's + [`ResourceLimits`](https://github.com/pydantic/monty) TypedDict + (`max_allocations`, `max_duration_secs`, `max_memory`, `gc_interval`, + `max_recursion_depth`). + +## DSL inside `execute_code` + +The model generates Python code that runs inside Monty's Rust-based interpreter. +Available primitives: + +| Primitive | Behavior | +|-----------|----------| +| `await tool_name(**kwargs)` | Direct typed call to a registered host tool. Argument types are checked before execution. | +| `await call_tool("name", **kwargs)` | Generic fallback that dispatches by tool name. Not type-checked. | +| `asyncio.gather(...)` | Fans out concurrent tool calls. | +| `print(...)` | Captured and surfaced as text in the tool result. | + +## Notes + +- `MontyCodeActProvider` and `MontyExecuteCodeTool` mirror the API surface of + the `agent-framework-hyperlight` counterparts where the underlying runtime + supports it. +- Monty interprets a **subset** of Python (a Rust-based interpreter). Most + control flow, common stdlib modules (`sys`, `os`, `typing`, `asyncio`, `re`, + `datetime`, `json`), and async functions are supported, but exotic features + may not be available. OS-level access (filesystem, network, subprocess) is + rejected with `PermissionError` **by default**; mount host directories with + `workspace_root` / `file_mounts` to grant scoped filesystem access. +- Code is type-checked against tool signatures via + [ty](https://docs.astral.sh/ty/) before execution, so wrong argument types + surface as a clear error before any host tool runs. +- The alpha package is **not** part of `agent-framework[all]` yet, so it must + be installed explicitly. Once promoted to beta it will be reachable via the + lazy-loading namespace `agent_framework.monty`. diff --git a/python/packages/monty/agent_framework_monty/__init__.py b/python/packages/monty/agent_framework_monty/__init__.py new file mode 100644 index 0000000000..cbeef88e49 --- /dev/null +++ b/python/packages/monty/agent_framework_monty/__init__.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import importlib.metadata + +from ._execute_code_tool import MontyExecuteCodeTool +from ._provider import MontyCodeActProvider +from ._types import FileMount, FileMountInput, MountMode + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" + +__all__ = [ + "FileMount", + "FileMountInput", + "MontyCodeActProvider", + "MontyExecuteCodeTool", + "MountMode", + "__version__", +] diff --git a/python/packages/monty/agent_framework_monty/_execute_code_tool.py b/python/packages/monty/agent_framework_monty/_execute_code_tool.py new file mode 100644 index 0000000000..4d5e957e4a --- /dev/null +++ b/python/packages/monty/agent_framework_monty/_execute_code_tool.py @@ -0,0 +1,558 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""``MontyExecuteCodeTool`` - a ``FunctionTool`` that runs Python in Monty. + +Mirrors the public API of ``HyperlightExecuteCodeTool`` for the subset that +applies to a pure-Python interpreter (no backends to choose from). By default +the Monty sandbox rejects OS / filesystem / network calls with +``PermissionError``; pass ``workspace_root`` or ``file_mounts`` to expose +scoped host directories, and the tool will capture any files written under +``read-write`` mounts as ``Content`` items in the response. +""" + +from __future__ import annotations + +import json +import mimetypes +from collections.abc import Callable, Iterator, Sequence +from copy import copy +from functools import partial +from pathlib import Path, PurePosixPath +from typing import Any, cast + +from agent_framework import Content, FunctionTool +from agent_framework._tools import ApprovalMode, normalize_tools + +from ._instructions import build_codeact_instructions, build_execute_code_description +from ._monty_bridge import InlineCodeBridge, generate_type_stubs +from ._types import FileMount, FileMountInput + +EXECUTE_CODE_TOOL_NAME = "execute_code" +EXECUTE_CODE_TOOL_DESCRIPTION = "Execute Python in a Monty interpreter." + +#: Virtual path that the optional ``workspace_root`` directory is mounted at, +#: matching the Hyperlight default. Use ``file_mounts`` for any other path. +WORKSPACE_MOUNT_PATH = "/input" + +#: Maximum bytes per captured output file. Files larger than this are skipped +#: and a ``Content.from_text`` warning is appended in their place. +MAX_CAPTURED_FILE_BYTES = 5 * 1024 * 1024 # 5 MiB + +EXECUTE_CODE_INPUT_SCHEMA: dict[str, Any] = { + "type": "object", + "title": "_ExecuteCodeInput", + "properties": { + "code": { + "type": "string", + "title": "Code", + "description": "Python code to execute in a Monty interpreter.", + }, + }, + "required": ["code"], +} + + +def _collect_tools(*tool_groups: Any) -> list[FunctionTool]: + """Merge tool groups, dropping any ``execute_code`` entries and deduping by name.""" + tools_by_name: dict[str, FunctionTool] = {} + + for tool_group in tool_groups: + normalized_group = normalize_tools(tool_group) + for tool_obj in normalized_group: + if not isinstance(tool_obj, FunctionTool): + continue + if tool_obj.name == EXECUTE_CODE_TOOL_NAME: + continue + tools_by_name.pop(tool_obj.name, None) + tools_by_name[tool_obj.name] = tool_obj + + return list(tools_by_name.values()) + + +def _resolve_execute_code_approval_mode( + *, + base_approval_mode: ApprovalMode, + tools: Sequence[FunctionTool], +) -> ApprovalMode: + if base_approval_mode == "always_require": + return "always_require" + if any(tool_obj.approval_mode == "always_require" for tool_obj in tools): + return "always_require" + return "never_require" + + +def _normalize_mount_path(mount_path: str) -> str: + """Normalize a virtual mount path to a clean POSIX absolute path.""" + raw = mount_path.strip().replace("\\", "/") + if not raw: + raise ValueError("mount_path must not be empty.") + pure = PurePosixPath(raw) + parts = [part for part in pure.parts if part not in {"", "/", "."}] + if any(part == ".." for part in parts): + raise ValueError("mount_path must not contain '..' segments.") + if not parts: + raise ValueError("mount_path must point to a concrete absolute path.") + return "/" + "/".join(parts) + + +def _resolve_existing_directory(value: str | Path) -> Path: + resolved = Path(value).expanduser().resolve(strict=True) + if not resolved.is_dir(): + raise ValueError(f"Path {value!r} must point to an existing directory.") + return resolved + + +def _is_file_mount_pair(value: Any) -> bool: + if not isinstance(value, tuple) or isinstance(value, FileMount): + return False + items = cast("tuple[object, ...]", value) + if len(items) != 2: + return False + host_path, mount_path = items + return isinstance(host_path, (str, Path)) and isinstance(mount_path, str) + + +def _normalize_file_mount(file_mount: FileMountInput) -> FileMount: + if isinstance(file_mount, FileMount): + host_path = file_mount.host_path + mount_path = file_mount.mount_path + mode = file_mount.mode + write_limit = file_mount.write_bytes_limit + elif isinstance(file_mount, str): + host_path = file_mount + mount_path = file_mount + mode = "overlay" + write_limit = None + else: + host_path, mount_path = file_mount + mode = "overlay" + write_limit = None + + return FileMount( + host_path=_resolve_existing_directory(host_path), + mount_path=_normalize_mount_path(mount_path), + mode=mode, + write_bytes_limit=write_limit, + ) + + +def _to_monty_mount(file_mount: FileMount) -> Any: + """Convert a public :class:`FileMount` to Monty's ``MountDir``. + + Imports lazily through the bridge's loader so missing-dependency errors + surface as the same actionable ``RuntimeError`` the rest of the package + raises, rather than a bare ``ImportError`` from a top-level import. + """ + from ._monty_bridge import load_monty # avoid top-level pydantic_monty import + + monty_module = load_monty() + return monty_module.MountDir( + virtual_path=file_mount.mount_path, + host_path=str(file_mount.host_path), + mode=file_mount.mode, + write_bytes_limit=file_mount.write_bytes_limit, + ) + + +def _make_tool_callback(tool_obj: FunctionTool) -> Callable[..., Any]: + """Return an async callable that invokes ``tool_obj`` with the bridge's kwargs. + + Returns the raw native value (no ``Content`` wrapping) so the Monty interpreter + receives real Python objects. ``FunctionTool.invoke`` accepts direct keyword + arguments and handles both sync and async underlying functions internally. + """ + return partial(copy(tool_obj).invoke, skip_parsing=True) + + +class MontyExecuteCodeTool(FunctionTool): + """Execute Python code inside a Monty interpreter. + + Tools registered on this object are available inside the interpreter as + typed async functions (e.g. ``await tool_name(...)``). Argument types are + validated by the [ty](https://docs.astral.sh/ty/) type checker before any + host tool runs. + + Optional filesystem access is exposed via: + + - ``workspace_root`` — auto-mounts a host directory at ``/input`` (matching + Hyperlight's default). + - ``file_mounts`` — extra :class:`FileMount` entries for fine-grained + control (mount path, read-only / read-write / overlay mode, write + byte caps). + + Files written by sandboxed code to any **read-write** mount are scanned + after execution and returned as ``Content.from_data`` items, mirroring + Hyperlight's ``/output`` flow. + + ``resource_limits`` is forwarded to Monty's ``ResourceLimits`` to cap CPU + time, memory, output size, recursion depth, and GC frequency. + + All mutators (``add_tools``, ``add_file_mounts`` etc.) must be called from + the same task/thread that owns the tool. Monty itself runs on the event + loop, so no internal locking is needed. + """ + + def __init__( + self, + *, + tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None, + approval_mode: ApprovalMode | None = None, + workspace_root: str | Path | None = None, + file_mounts: FileMountInput | Sequence[FileMountInput] | None = None, + resource_limits: dict[str, Any] | None = None, + ) -> None: + super().__init__( + name=EXECUTE_CODE_TOOL_NAME, + description=EXECUTE_CODE_TOOL_DESCRIPTION, + approval_mode="never_require", + func=self._run_code, + input_model=EXECUTE_CODE_INPUT_SCHEMA, + ) + self._default_approval_mode: ApprovalMode = approval_mode or "never_require" + self._managed_tools: list[FunctionTool] = [] + self._workspace_root: Path | None = ( + _resolve_existing_directory(workspace_root) if workspace_root is not None else None + ) + self._file_mounts: dict[str, FileMount] = {} + self._resource_limits: dict[str, Any] | None = dict(resource_limits) if resource_limits else None + + if tools is not None: + self.add_tools(tools) + if file_mounts is not None: + self.add_file_mounts(file_mounts) + + self._refresh_approval_mode() + + @property + def description(self) -> str: + # During FunctionTool.__init__, ``_managed_tools`` is not yet set. + if not hasattr(self, "_managed_tools"): + return str(self.__dict__.get("description", EXECUTE_CODE_TOOL_DESCRIPTION)) + return build_execute_code_description( + tools=self._managed_tools, + mounts=self._effective_mounts(), + ) + + @description.setter + def description(self, value: str) -> None: + self.__dict__["description"] = value + + def add_tools( + self, + tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]], + ) -> None: + """Add Monty-side tools to this execute_code surface.""" + self._managed_tools = _collect_tools(self._managed_tools, tools) + self._refresh_approval_mode() + + def get_tools(self) -> list[FunctionTool]: + """Return the currently managed Monty tools.""" + return list(self._managed_tools) + + def remove_tool(self, name: str) -> None: + """Remove one managed Monty tool by name.""" + remaining_tools = [tool_obj for tool_obj in self._managed_tools if tool_obj.name != name] + if len(remaining_tools) == len(self._managed_tools): + raise KeyError(f"No managed tool named {name!r} is registered.") + self._managed_tools = remaining_tools + self._refresh_approval_mode() + + def clear_tools(self) -> None: + """Remove all managed Monty tools.""" + self._managed_tools = [] + self._refresh_approval_mode() + + def add_file_mounts(self, file_mounts: FileMountInput | Sequence[FileMountInput]) -> None: + """Add one or more file mounts. + + A single string mounts the same path on both sides. Use a + ``(host_path, mount_path)`` tuple or :class:`FileMount` when the paths + differ or when you need to set the mount mode / write limit. + """ + if isinstance(file_mounts, (str, FileMount)) or _is_file_mount_pair(file_mounts): + normalized = [_normalize_file_mount(cast("FileMountInput", file_mounts))] + else: + normalized = [_normalize_file_mount(item) for item in cast("Sequence[FileMountInput]", file_mounts)] + + for mount in normalized: + self._file_mounts[mount.mount_path] = mount + + def get_file_mounts(self) -> list[FileMount]: + """Return the configured file mounts (excluding ``workspace_root``).""" + return list(self._file_mounts.values()) + + def remove_file_mount(self, mount_path: str) -> None: + """Remove one file mount by its sandbox path.""" + normalized = _normalize_mount_path(mount_path) + if normalized not in self._file_mounts: + raise KeyError(f"No file mount exists for {mount_path!r}.") + del self._file_mounts[normalized] + + def clear_file_mounts(self) -> None: + """Remove all configured file mounts.""" + self._file_mounts.clear() + + @property + def workspace_root(self) -> Path | None: + """Return the configured workspace root, if any.""" + return self._workspace_root + + @property + def resource_limits(self) -> dict[str, Any] | None: + """Return the configured Monty :class:`pydantic_monty.ResourceLimits`, if any.""" + return dict(self._resource_limits) if self._resource_limits else None + + def build_instructions(self, *, tools_visible_to_model: bool) -> str: + """Build the current CodeAct instructions for this execute_code surface.""" + return build_codeact_instructions( + tools=list(self._managed_tools), + tools_visible_to_model=tools_visible_to_model, + mounts=self._effective_mounts(), + ) + + def create_run_tool(self) -> MontyExecuteCodeTool: + """Create a run-scoped snapshot of this execute_code surface.""" + return MontyExecuteCodeTool( + tools=self.get_tools(), + approval_mode=self._default_approval_mode, + workspace_root=self._workspace_root, + file_mounts=list(self._file_mounts.values()) or None, + resource_limits=self._resource_limits, + ) + + def build_serializable_state(self) -> dict[str, Any]: + """Return a JSON-serializable snapshot of the effective run state.""" + approval_mode = _resolve_execute_code_approval_mode( + base_approval_mode=self._default_approval_mode, + tools=self._managed_tools, + ) + mounts = self._effective_mounts() + return { + "runtime": "monty", + "approval_mode": approval_mode, + "tool_names": [tool_obj.name for tool_obj in self._managed_tools], + "workspace_root": str(self._workspace_root) if self._workspace_root is not None else None, + "file_mounts": [ + { + "host_path": str(mount.host_path), + "mount_path": mount.mount_path, + "mode": mount.mode, + "write_bytes_limit": mount.write_bytes_limit, + } + for mount in mounts + ], + "resource_limits": dict(self._resource_limits) if self._resource_limits else None, + } + + def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) -> dict[str, Any]: + # Materialize the dynamic description so the dump captures the current tool list. + self.__dict__["description"] = self.description + return super().to_dict(exclude=exclude, exclude_none=exclude_none) + + def _refresh_approval_mode(self) -> None: + self.approval_mode = _resolve_execute_code_approval_mode( + base_approval_mode=self._default_approval_mode, + tools=self._managed_tools, + ) + + def _build_tool_map(self, tools: Sequence[FunctionTool]) -> dict[str, Callable[..., Any]]: + return {tool_obj.name: _make_tool_callback(tool_obj) for tool_obj in tools} + + def _build_type_stub_map(self, tools: Sequence[FunctionTool]) -> dict[str, Callable[..., Any]]: + """Return a name -> underlying-Python-callable map for type stub generation. + + The raw Python function attached to the ``FunctionTool`` carries the + author's actual ``Annotated`` parameter types, which are what we want + ``ty`` to validate against. Tools without an attached function (e.g. + ``declaration_only`` tools) are skipped. + """ + stub_map: dict[str, Callable[..., Any]] = {} + for tool_obj in tools: + func = getattr(tool_obj, "func", None) + if callable(func): + stub_map[tool_obj.name] = func + return stub_map + + def _effective_mounts(self) -> list[FileMount]: + """Combine ``workspace_root`` (if set) with the explicit ``file_mounts``.""" + mounts: list[FileMount] = [] + if self._workspace_root is not None and WORKSPACE_MOUNT_PATH not in self._file_mounts: + mounts.append( + FileMount( + host_path=self._workspace_root, + mount_path=WORKSPACE_MOUNT_PATH, + mode="read-write", + write_bytes_limit=None, + ) + ) + mounts.extend(self._file_mounts.values()) + return mounts + + async def _run_code(self, *, code: str) -> list[Content]: + tools = list(self._managed_tools) + mounts = self._effective_mounts() + + tool_map = self._build_tool_map(tools) + stub_map = self._build_type_stub_map(tools) + type_stubs = generate_type_stubs(stub_map) if stub_map else None + + # Snapshot mtimes of host files in read-write mounts so we can later + # identify which files the sandbox actually touched. + pre_state = _snapshot_writable_mounts(mounts) + + bridge = InlineCodeBridge( + tool_map, + type_stubs=type_stubs, + mounts=[_to_monty_mount(mount) for mount in mounts] or None, + resource_limits=self._resource_limits, + ) + + try: + result = await bridge.run(code) + except Exception as exc: + return [ + Content.from_error( + message="Execution error", + error_details=f"{type(exc).__name__}: {exc}", + ), + ] + + contents = _build_execution_contents(result=result) + contents.extend(_capture_written_files(mounts, pre_state)) + return contents + + +def _build_execution_contents(*, result: dict[str, Any]) -> list[Content]: + stdout = str(result.get("stdout") or "").replace("\r\n", "\n") + output_value = result.get("output") + truncated = bool(result.get("truncated")) + + outputs: list[Content] = [] + if stdout: + text = stdout + if truncated: + text = f"{text}\n\n[stdout truncated]" + outputs.append(Content.from_text(text)) + elif truncated: + outputs.append(Content.from_text("[stdout truncated]")) + + if output_value is not None: + try: + serialized_output = json.dumps(output_value, ensure_ascii=False) + except (TypeError, ValueError): + serialized_output = repr(output_value) + outputs.append(Content.from_text(serialized_output)) + + if not outputs: + outputs.append(Content.from_text("Code executed successfully without output.")) + + return outputs + + +def _iter_real_files(root: Path) -> Iterator[Path]: + """Walk ``root`` recursively, yielding only real (non-symlink) files. + + ``Path.rglob`` follows directory symlinks by default, which combined with + ``Path.is_file()`` / ``Path.read_bytes()`` (both follow symlinks) would let + an attacker who controls the workspace pre-place a symlink to a host file + or directory and have our post-execution capture surface it. Skipping every + symlink at both the directory and file level closes that escape. + """ + stack: list[Path] = [root] + while stack: + current = stack.pop() + try: + entries = list(current.iterdir()) + except OSError: + continue + for entry in entries: + try: + if entry.is_symlink(): + continue + if entry.is_dir(): + stack.append(entry) + elif entry.is_file(): + yield entry + except OSError: + continue + + +def _snapshot_writable_mounts(mounts: Sequence[FileMount]) -> dict[str, dict[str, tuple[int, int]]]: + """Capture (size, mtime_ns) for every real (non-symlink) host file under read-write mounts. + + Returns ``{mount_path: {relative_posix_path: (size, mtime_ns)}}``. Used by + :func:`_capture_written_files` to detect new or modified files after the run. + Read-only and overlay mounts are skipped because their writes do not + propagate to the host. Symlinks (file or directory) are deliberately skipped + so an attacker cannot escape the mount by pre-placing a symlink to a host + path outside the workspace. + """ + snapshot: dict[str, dict[str, tuple[int, int]]] = {} + for mount in mounts: + if mount.mode != "read-write": + continue + host_root = Path(mount.host_path) + per_mount: dict[str, tuple[int, int]] = {} + for entry in _iter_real_files(host_root): + try: + stat = entry.lstat() # lstat: never follow symlinks (defensive) + except OSError: + continue + relative = entry.relative_to(host_root).as_posix() + per_mount[relative] = (int(stat.st_size), int(stat.st_mtime_ns)) + snapshot[mount.mount_path] = per_mount + return snapshot + + +def _capture_written_files( + mounts: Sequence[FileMount], + pre_state: dict[str, dict[str, tuple[int, int]]], +) -> list[Content]: + """Return :class:`Content` items for files the sandbox wrote during the run. + + Mirrors Hyperlight's ``/output`` capture flow: any new or modified real + (non-symlink) file under a read-write mount is read back as binary and + surfaced as ``Content.from_data`` with a ``path`` annotation in + ``additional_properties``. Symlinks are skipped at both directory and file + level so a malicious workspace cannot trick us into capturing host files + outside the configured mount root. + """ + captured: list[Content] = [] + for mount in mounts: + if mount.mode != "read-write": + continue + host_root = Path(mount.host_path) + before = pre_state.get(mount.mount_path, {}) + for entry in sorted(_iter_real_files(host_root)): + try: + stat = entry.lstat() + except OSError: + continue + relative = entry.relative_to(host_root).as_posix() + current = (int(stat.st_size), int(stat.st_mtime_ns)) + if before.get(relative) == current: + continue # Unchanged. + sandbox_path = f"{mount.mount_path.rstrip('/')}/{relative}" + if stat.st_size > MAX_CAPTURED_FILE_BYTES: + captured.append( + Content.from_text( + f"[file {sandbox_path} omitted: {stat.st_size} bytes " + f"exceeds MAX_CAPTURED_FILE_BYTES={MAX_CAPTURED_FILE_BYTES}]" + ) + ) + continue + try: + # _iter_real_files already excluded symlinks at every level of + # the walk; reading the file here is safe. + data = entry.read_bytes() + except OSError: + continue + media_type = mimetypes.guess_type(entry.name)[0] or "application/octet-stream" + captured.append( + Content.from_data( + data=data, + media_type=media_type, + additional_properties={"path": sandbox_path}, + ) + ) + return captured diff --git a/python/packages/monty/agent_framework_monty/_instructions.py b/python/packages/monty/agent_framework_monty/_instructions.py new file mode 100644 index 0000000000..c560e356d3 --- /dev/null +++ b/python/packages/monty/agent_framework_monty/_instructions.py @@ -0,0 +1,125 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Dynamic CodeAct instructions and execute_code tool descriptions for Monty.""" + +from __future__ import annotations + +from collections.abc import Sequence + +from agent_framework import FunctionTool + +from ._types import FileMount + + +def _format_tool_summaries(tools: Sequence[FunctionTool]) -> str: + if not tools: + return "- No tools are currently registered." + + lines: list[str] = [] + for tool_obj in tools: + parameters = tool_obj.parameters().get("properties", {}) + parameter_names = [name for name in parameters if isinstance(name, str)] + parameter_summary = ", ".join(parameter_names) if parameter_names else "none" + description = str(tool_obj.description or "").strip() or "No description provided." + lines.append(f"- `{tool_obj.name}`: {description} Parameters: {parameter_summary}.") + return "\n".join(lines) + + +def _format_filesystem_capabilities(mounts: Sequence[FileMount]) -> str: + if not mounts: + return ( + "Filesystem access is unavailable. OS-level paths raise `PermissionError`. " + "If you need files, ask the agent operator to configure `workspace_root` or `file_mounts`." + ) + + lines = ["Filesystem access is enabled. Read and write paths via `pathlib.Path(...)` (or `os.path`)."] + lines.append("Configured mounts:") + for mount in mounts: + cap = "" + if mount.write_bytes_limit is not None: + cap = f", write cap {mount.write_bytes_limit} bytes" + lines.append(f"- `{mount.mount_path}` ({mount.mode}{cap})") + + writable = [mount for mount in mounts if mount.mode == "read-write"] + if writable: + writable_paths = ", ".join(f"`{m.mount_path}`" for m in writable) + lines.append( + f"Files written to {writable_paths} are returned to the caller as attached files; " + "use these paths for any output artifacts." + ) + + return "\n".join(lines) + + +def build_codeact_instructions( + *, + tools: Sequence[FunctionTool], + tools_visible_to_model: bool, + mounts: Sequence[FileMount] = (), +) -> str: + """Build dynamic CodeAct instructions for the effective Monty tool set.""" + tool_summaries = _format_tool_summaries(tools) + filesystem_text = _format_filesystem_capabilities(mounts) + + usage_note = ( + "Some tools may also appear directly, but prefer `execute_code` whenever you need to combine " + "Python control flow with sandbox tool calls." + if tools_visible_to_model + else "Provider-owned sandbox tools are not exposed separately; use `execute_code` when you need them." + ) + + return f"""You have one primary tool: `execute_code`. + +Inside `execute_code`, call registered tools directly as async functions: +`result = await tool_name(param=value)`. Always use `await` and keyword arguments. +Your code is type-checked against the tool signatures below before execution. +`await call_tool('name', **kwargs)` is also supported as a fallback but is not type-checked. + +For fan-out, use `asyncio.gather`: +`results = await asyncio.gather(tool_a(...), tool_b(...))`. + +Surface results to the caller via `print(...)` (captured and returned as text) +or by ending the code with an expression whose value is JSON-encodable - the +value of the final expression is returned alongside captured stdout. + +Filesystem capabilities: +{filesystem_text} + +Registered tools: +{tool_summaries} + +Prefer a single `execute_code` call per request when possible, combining +multiple tool calls with Python control flow. + +{usage_note} +""" + + +def build_execute_code_description( + *, + tools: Sequence[FunctionTool], + mounts: Sequence[FileMount] = (), +) -> str: + """Build the dynamic ``execute_code`` tool description for standalone usage.""" + tool_summaries = _format_tool_summaries(tools) + filesystem_text = _format_filesystem_capabilities(mounts) + + return f"""Execute Python code in a Monty interpreter. + +Inside the sandbox, call registered tools directly as typed async functions: +`result = await tool_name(param=value)`. Always use `await` and keyword arguments. +Code is type-checked against tool signatures before execution. +`await call_tool('name', **kwargs)` is also supported as a fallback. + +For fan-out, use `asyncio.gather`: +`results = await asyncio.gather(tool_a(...), tool_b(...))`. + +Filesystem capabilities: +{filesystem_text} + +Registered tools: +{tool_summaries} + +Surface results via `print(...)` (captured and returned as text) or by ending +with an expression whose value is JSON-encodable. +""" diff --git a/python/packages/monty/agent_framework_monty/_monty_bridge.py b/python/packages/monty/agent_framework_monty/_monty_bridge.py new file mode 100644 index 0000000000..1d9cd46c40 --- /dev/null +++ b/python/packages/monty/agent_framework_monty/_monty_bridge.py @@ -0,0 +1,327 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Inline (non-durable) Monty execution bridge and type-stub generation. + +Adapted from https://github.com/anthonychu/maf-codeact-monty-python. +""" + +from __future__ import annotations + +import asyncio +import inspect +import keyword +import types +import typing +from collections.abc import Callable, Sequence +from typing import Annotated, Any, cast, get_type_hints + +MAX_PRINT_OUTPUT_CHARS = 8192 + +# Prelude injected into all Monty code so `asyncio.gather` works for fan-out. +_CODEACT_PRELUDE = """\ +import asyncio +""" + + +def _ensure_json_value(value: Any) -> Any: + if value is None or isinstance(value, (str, bool, int)): + return value + if isinstance(value, float): + if value != value or value in (float("inf"), float("-inf")): + raise ValueError("Non-finite floating point values are not JSON-safe.") + return value + if isinstance(value, (list, tuple)): + items = cast("list[object] | tuple[object, ...]", value) + return [_ensure_json_value(item) for item in items] + if isinstance(value, dict): + as_dict = cast("dict[object, object]", value) + return {str(k): _ensure_json_value(v) for k, v in as_dict.items()} + raise ValueError(f"Value of type {type(value).__name__} is not JSON-safe.") + + +def _external_error(exc: Exception) -> dict[str, str]: + return {"exc_type": type(exc).__name__, "message": str(exc)} + + +def _parse_call_tool(args: tuple[Any, ...], kwargs: dict[str, Any]) -> tuple[str, dict[str, Any]]: + if not args: + raise ValueError("call_tool requires a tool name as the first argument.") + name = args[0] + if not isinstance(name, str) or not name: + raise ValueError("Tool name must be a non-empty string.") + if len(args) > 1: + raise ValueError( + "call_tool accepts only the tool name as a positional argument. Use keyword arguments for parameters." + ) + return name, dict(kwargs) + + +def _build_code(code: str) -> str: + return f"{_CODEACT_PRELUDE}\n{code}" + + +def _python_type_repr(annotation: Any) -> str: + """Convert a Python type annotation to its string representation for stubs.""" + if annotation is inspect.Parameter.empty: + return "Any" + if annotation is type(None): + # ``None`` in annotations represents ``NoneType``; emit it literally so + # ``ty`` can validate ``Optional[X]`` / ``Union[..., None]`` / ``-> None`` + # signatures correctly. + return "None" + origin = typing.get_origin(annotation) + if origin is Annotated: + args = typing.get_args(annotation) + return _python_type_repr(args[0]) if args else "Any" + if origin is not None: + args = typing.get_args(annotation) + # Normalize ``typing.Union[...]`` and PEP-604 ``X | Y`` to PEP-604 syntax so + # ``None`` is preserved across both forms. + if origin is typing.Union or origin is types.UnionType: + return " | ".join(_python_type_repr(a) for a in args) if args else "Any" + origin_name = getattr(origin, "__name__", None) + if origin_name is None: + origin_name = str(origin) + if origin_name.startswith(" str: + """Generate Python type stub declarations for tools + DSL primitives. + + Stubs are fed to Monty's ``type_check_stubs`` so ``ty`` can validate the + LLM-generated code against the actual tool signatures before any host + call runs. + + Tools whose ``name`` is not a valid Python identifier are skipped because + their name cannot be safely splatted into stub source. The model can still + reach them via the ``call_tool("weird name", ...)`` fallback at runtime, + but they will not get type-checked stubs. + """ + lines: list[str] = [ + "from typing import Any", + "", + "# DSL primitives", + "async def call_tool(name: str, **kwargs: Any) -> Any:", + " raise NotImplementedError()", + "", + "# Registered tools - call directly with typed arguments", + ] + + for name, func in sorted(tool_callables.items()): + if not name.isidentifier() or keyword.iskeyword(name): + # A non-identifier name (or a Python keyword) would inject invalid + # / dangerous syntax into the stub source. Skip stub generation; + # the tool stays reachable through ``call_tool(name, ...)``. + continue + try: + sig = inspect.signature(func) + hints = get_type_hints(func, include_extras=True) + except (ValueError, TypeError): + lines.append(f"async def {name}(**kwargs: Any) -> Any:") + lines.append(" raise NotImplementedError()") + lines.append("") + continue + + params: list[str] = [] + for param_name, param in sig.parameters.items(): + annotation = hints.get(param_name, inspect.Parameter.empty) + type_str = _python_type_repr(annotation) + if param.default is not inspect.Parameter.empty: + params.append(f"{param_name}: {type_str} = ...") + else: + params.append(f"{param_name}: {type_str}") + + return_annotation = hints.get("return", inspect.Parameter.empty) + return_str = _python_type_repr(return_annotation) + param_str = ", ".join(params) + lines.append(f"async def {name}({param_str}) -> {return_str}:") + lines.append(" raise NotImplementedError()") + lines.append("") + + return "\n".join(lines) + + +class _PrintCollector: + """Collect Monty stdout, capped at ``MAX_PRINT_OUTPUT_CHARS``.""" + + def __init__(self) -> None: + self.chunks: list[str] = [] + self.truncated: bool = False + self._size: int = 0 # running character count to avoid O(n) per append + + def __call__(self, stream: str, text: str) -> None: + if self.truncated: + return + remaining = MAX_PRINT_OUTPUT_CHARS - self._size + if remaining <= 0: + self.truncated = True + return + text_value = str(text) + if len(text_value) > remaining: + clipped = text_value[:remaining] + self.chunks.append(clipped) + self._size += len(clipped) + self.truncated = True + else: + self.chunks.append(text_value) + self._size += len(text_value) + + @property + def output(self) -> str: + return "".join(self.chunks) + + +def load_monty() -> Any: + """Import ``pydantic_monty`` lazily so unit tests can run without it. + + Returns the module so callers can read ``Monty``, ``MontyComplete``, + ``FunctionSnapshot``, ``FutureSnapshot``, ``NameLookupSnapshot`` from it. + """ + try: + import pydantic_monty # type: ignore[import-not-found] + except ImportError as exc: + raise RuntimeError( + "The `pydantic-monty` package is required to execute Monty CodeAct code. " + "Install it with `pip install pydantic-monty`." + ) from exc + return pydantic_monty + + +class InlineCodeBridge: + """Execute Monty code inline (non-durable). + + Supports both ``await call_tool('name', ...)`` and direct ``await name(...)`` + calls. When Monty yields a :class:`FutureSnapshot`, the bridge invokes the + registered host tools and resumes execution with the results. + """ + + def __init__( + self, + tool_map: dict[str, Callable[..., Any]], + *, + type_stubs: str | None = None, + mounts: Sequence[Any] | None = None, + resource_limits: dict[str, Any] | None = None, + ) -> None: + self.tool_map: dict[str, Callable[..., Any]] = dict(tool_map) + self.type_stubs: str | None = type_stubs + self._mounts = tuple(mounts) if mounts else () + self._resource_limits = resource_limits + self._pending_calls: dict[int, tuple[str, dict[str, Any]]] = {} + + async def run(self, code: str) -> dict[str, Any]: + if not isinstance(code, str) or not code.strip(): + raise ValueError("Code must be a non-empty string.") + + monty_module = load_monty() + Monty = monty_module.Monty + MontyComplete = monty_module.MontyComplete + FunctionSnapshot = monty_module.FunctionSnapshot + FutureSnapshot = monty_module.FutureSnapshot + NameLookupSnapshot = monty_module.NameLookupSnapshot + + printer = _PrintCollector() + monty = Monty( + _build_code(code), + script_name="codeact.py", + type_check=self.type_stubs is not None, + type_check_stubs=self.type_stubs, + ) + start_kwargs: dict[str, Any] = {"print_callback": printer} + if self._mounts: + start_kwargs["mount"] = list(self._mounts) + if self._resource_limits: + start_kwargs["limits"] = self._resource_limits + progress = monty.start(**start_kwargs) + + while True: + if isinstance(progress, MontyComplete): + return { + "output": _ensure_json_value(progress.output), + "stdout": printer.output, + "truncated": printer.truncated, + } + if isinstance(progress, FunctionSnapshot): + progress = self._handle_function(progress) + continue + if isinstance(progress, FutureSnapshot): + progress = await self._handle_future(progress) + continue + if isinstance(progress, NameLookupSnapshot): + raise RuntimeError(f"Name lookup not supported: {progress.variable_name!r}") + raise RuntimeError(f"Unsupported Monty progress type: {type(progress).__name__}") + + def _handle_function(self, snapshot: Any) -> Any: + if snapshot.is_os_function: + return snapshot.resume({ + "exc_type": "PermissionError", + "message": "OS and filesystem calls are not available.", + }) + + function_name = str(snapshot.function_name) + + if function_name in self.tool_map: + return self._schedule_direct_tool(snapshot, function_name) + if function_name == "call_tool": + return self._schedule_call_tool(snapshot) + + return snapshot.resume({ + "exc_type": "NameError", + "message": f"Function {function_name!r} is not available.", + }) + + def _schedule_direct_tool(self, snapshot: Any, name: str) -> Any: + # Positional args are rejected up-front by ``ty`` because the generated + # stubs declare every parameter as keyword-typed. Anything that slips + # through (e.g. tools with no signature inspection) is forwarded to the + # host tool as-is via kwargs only. + self._pending_calls[int(snapshot.call_id)] = (name, dict(snapshot.kwargs)) + return snapshot.resume({"future": ...}) + + def _schedule_call_tool(self, snapshot: Any) -> Any: + try: + name, kwargs = _parse_call_tool(snapshot.args, snapshot.kwargs) + if name not in self.tool_map: + allowed = ", ".join(sorted(self.tool_map.keys())) or "" + raise ValueError(f"Tool {name!r} is not registered. Available tools: {allowed}") + self._pending_calls[int(snapshot.call_id)] = (name, kwargs) + except Exception as exc: + return snapshot.resume(_external_error(exc)) + return snapshot.resume({"future": ...}) + + async def _handle_future(self, snapshot: Any) -> Any: + pending_call_ids = [int(cid) for cid in snapshot.pending_call_ids] + if not pending_call_ids: + return snapshot.resume({}) + + entries: list[tuple[int, tuple[str, dict[str, Any]]]] = [] + for cid in pending_call_ids: + if cid not in self._pending_calls: + raise RuntimeError(f"Unknown future call ID: {cid}") + entries.append((cid, self._pending_calls.pop(cid))) + + tasks = [self._invoke_tool(cid, name, kwargs) for cid, (name, kwargs) in entries] + results = await asyncio.gather(*tasks) + resume_results: dict[int, Any] = dict(results) + return snapshot.resume(resume_results) + + async def _invoke_tool(self, cid: int, name: str, kwargs: dict[str, Any]) -> tuple[int, Any]: + # Every entry in ``self.tool_map`` is produced by ``_make_tool_callback`` + # as ``partial(FunctionTool.invoke, skip_parsing=True)``. ``FunctionTool.invoke`` + # is always ``async def``, so a plain ``await`` is correct for every call and + # avoids relying on ``inspect.iscoroutinefunction(partial(...))``, which can + # return ``False`` for some ``partial`` shapes (cpython#98590) and would route + # the call through ``asyncio.to_thread`` with an unawaited coroutine return. + try: + result = await self.tool_map[name](**kwargs) + return cid, {"return_value": _ensure_json_value(result)} + except Exception as exc: + return cid, _external_error(exc) diff --git a/python/packages/monty/agent_framework_monty/_provider.py b/python/packages/monty/agent_framework_monty/_provider.py new file mode 100644 index 0000000000..abec2a33fa --- /dev/null +++ b/python/packages/monty/agent_framework_monty/_provider.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""``MontyCodeActProvider`` - context provider injecting Monty-backed CodeAct.""" + +from __future__ import annotations + +from collections.abc import Callable, Sequence +from pathlib import Path +from typing import Any + +from agent_framework import AgentSession, ContextProvider, FunctionTool, SessionContext +from agent_framework._tools import ApprovalMode + +from ._execute_code_tool import MontyExecuteCodeTool +from ._types import FileMount, FileMountInput + + +class MontyCodeActProvider(ContextProvider): + """Inject a Monty-backed CodeAct surface using provider-owned tools. + + Mirrors :class:`agent_framework_hyperlight.HyperlightCodeActProvider` for + the subset of capabilities that apply to the Monty interpreter: + ``tools``, ``approval_mode``, ``workspace_root``, ``file_mounts``, and + ``resource_limits`` (Monty-only). + """ + + DEFAULT_SOURCE_ID = "monty_codeact" + + def __init__( + self, + source_id: str = DEFAULT_SOURCE_ID, + *, + tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None, + approval_mode: ApprovalMode | None = None, + workspace_root: str | Path | None = None, + file_mounts: FileMountInput | Sequence[FileMountInput] | None = None, + resource_limits: dict[str, Any] | None = None, + ) -> None: + super().__init__(source_id) + self._execute_code_tool = MontyExecuteCodeTool( + tools=tools, + approval_mode=approval_mode, + workspace_root=workspace_root, + file_mounts=file_mounts, + resource_limits=resource_limits, + ) + + def add_tools( + self, + tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]], + ) -> None: + """Add provider-owned Monty tools.""" + self._execute_code_tool.add_tools(tools) + + def get_tools(self) -> list[FunctionTool]: + """Return the provider-owned Monty tools.""" + return self._execute_code_tool.get_tools() + + def remove_tool(self, name: str) -> None: + """Remove one provider-owned Monty tool by name.""" + self._execute_code_tool.remove_tool(name) + + def clear_tools(self) -> None: + """Remove all provider-owned Monty tools.""" + self._execute_code_tool.clear_tools() + + def add_file_mounts(self, file_mounts: FileMountInput | Sequence[FileMountInput]) -> None: + """Add provider-managed file mounts.""" + self._execute_code_tool.add_file_mounts(file_mounts) + + def get_file_mounts(self) -> list[FileMount]: + """Return the provider-managed file mounts (excluding ``workspace_root``).""" + return self._execute_code_tool.get_file_mounts() + + def remove_file_mount(self, mount_path: str) -> None: + """Remove one provider-managed file mount by its sandbox path.""" + self._execute_code_tool.remove_file_mount(mount_path) + + def clear_file_mounts(self) -> None: + """Remove all provider-managed file mounts.""" + self._execute_code_tool.clear_file_mounts() + + async def before_run( + self, + *, + agent: Any, + session: AgentSession | None, + context: SessionContext, + state: dict[str, Any], + ) -> None: + """Inject CodeAct instructions and a run-scoped execute_code tool before each run.""" + run_tool = self._execute_code_tool.create_run_tool() + state[self.source_id] = run_tool.build_serializable_state() + context.extend_instructions(self.source_id, run_tool.build_instructions(tools_visible_to_model=False)) + context.extend_tools(self.source_id, [run_tool]) diff --git a/python/packages/monty/agent_framework_monty/_types.py b/python/packages/monty/agent_framework_monty/_types.py new file mode 100644 index 0000000000..2072c39f4e --- /dev/null +++ b/python/packages/monty/agent_framework_monty/_types.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Public types for ``agent-framework-monty``. + +Mirrors ``agent_framework_hyperlight._types`` where the Monty runtime exposes +an equivalent concept so users can move between the two providers with minimal +churn. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Literal, NamedTuple, TypeAlias + +#: Allowed Monty mount modes. ``overlay`` (the Monty default) buffers writes +#: in-memory and is therefore not visible to the host after execution. +#: ``read-only`` rejects writes. ``read-write`` writes through to the host +#: directory. +MountMode: TypeAlias = Literal["overlay", "read-only", "read-write"] + + +class FileMount(NamedTuple): + """Map a host directory into the Monty sandbox. + + Mirrors :class:`agent_framework_hyperlight.FileMount` with two extra + fields that surface Monty's underlying ``MountDir`` capabilities: + ``mode`` selects read-only / read-write / overlay semantics, and + ``write_bytes_limit`` caps the total bytes written through this mount. + """ + + host_path: str | Path + mount_path: str + mode: MountMode = "overlay" + write_bytes_limit: int | None = None + + +FileMountHostPath: TypeAlias = str | Path +FileMountInput: TypeAlias = str | tuple[FileMountHostPath, str] | FileMount diff --git a/python/packages/monty/agent_framework_monty/py.typed b/python/packages/monty/agent_framework_monty/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/packages/monty/pyproject.toml b/python/packages/monty/pyproject.toml new file mode 100644 index 0000000000..802836913e --- /dev/null +++ b/python/packages/monty/pyproject.toml @@ -0,0 +1,107 @@ +[project] +name = "agent-framework-monty" +description = "Monty CodeAct integrations for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "1.0.0a260518" +license-files = ["LICENSE"] +urls.homepage = "https://aka.ms/agent-framework" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "agent-framework-core>=1.4.0,<2", + "pydantic-monty>=0,<0.1", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" + +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [] +timeout = 120 +markers = [ + "integration: marks tests as integration tests that require external services", +] + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["D", "INP", "TD", "ERA001", "RUF", "S"] + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extends = "../../pyproject.toml" +include = ["agent_framework_monty"] +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true + +[tool.bandit] +targets = ["agent_framework_monty"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" + +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_monty" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_monty --cov-report=term-missing:skip-covered tests' + +[tool.poe.tasks.test-integration] +help = "Run integration tests for this package (requires pydantic-monty)." +cmd = 'pytest -m "integration" tests' + +[tool.flit.module] +name = "agent_framework_monty" + +[build-system] +requires = ["flit-core >= 3.11,<4.0"] +build-backend = "flit_core.buildapi" diff --git a/python/packages/monty/tests/monty/test_monty_codeact.py b/python/packages/monty/tests/monty/test_monty_codeact.py new file mode 100644 index 0000000000..43c8e3acac --- /dev/null +++ b/python/packages/monty/tests/monty/test_monty_codeact.py @@ -0,0 +1,642 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Hermetic unit tests for ``agent_framework_monty``. + +These tests inject a fake Monty runtime via ``monkeypatch`` so they run without +the real ``pydantic-monty`` package doing any work. End-to-end tests against +the real runtime live in ``test_monty_codeact_integration.py``. +""" + +from __future__ import annotations + +import json +import sys +import types +from collections.abc import Iterable, Iterator +from dataclasses import dataclass, field +from pathlib import Path +from typing import Annotated, Any +from unittest.mock import MagicMock + +import pytest +from agent_framework import Content, FunctionTool, Message, tool +from agent_framework._sessions import SessionContext + +from agent_framework_monty import MontyCodeActProvider, MontyExecuteCodeTool +from agent_framework_monty import _execute_code_tool as execute_code_module +from agent_framework_monty import _monty_bridge as bridge_module + +# --------------------------------------------------------------------------- +# Fake Monty runtime - drop-in replacement for pydantic_monty +# --------------------------------------------------------------------------- + + +@dataclass +class _FakeMontyComplete: + output: Any = None + + +@dataclass +class _FakeFunctionSnapshot: + function_name: str + call_id: int + args: tuple[Any, ...] = () + kwargs: dict[str, Any] = field(default_factory=dict) + is_os_function: bool = False + _script: _FakeScript | None = None + + def resume(self, payload: Any) -> Any: + assert self._script is not None, "Snapshot must be attached to a script." + return self._script.advance(("function_resume", self, payload)) + + +@dataclass +class _FakeFutureSnapshot: + pending_call_ids: list[int] + _script: _FakeScript | None = None + + def resume(self, payload: Any) -> Any: + assert self._script is not None, "Snapshot must be attached to a script." + return self._script.advance(("future_resume", self, payload)) + + +@dataclass +class _FakeNameLookupSnapshot: + variable_name: str + + +@dataclass +class _PrintAction: + """Marker pushed onto a script to emit captured stdout via the print callback.""" + + text: str + + +class _FakeScript: + """Replayable Monty progress script with a resume log.""" + + def __init__(self, items: Iterable[Any]) -> None: + self._queue: list[Any] = list(items) + self.resume_log: list[tuple[str, Any, Any]] = [] + + def attach(self, snapshot: Any) -> Any: + snapshot._script = self + return snapshot + + def next_item(self) -> Any: + if not self._queue: + return _FakeMontyComplete(output=None) + item = self._queue.pop(0) + if isinstance(item, _FakeMontyComplete): + return item + if isinstance(item, _PrintAction): + return item + if isinstance(item, _FakeNameLookupSnapshot): + return item + return self.attach(item) + + def advance(self, log_entry: tuple[str, Any, Any]) -> Any: + self.resume_log.append(log_entry) + return self.next_item() + + +_current_script: list[_FakeScript | None] = [None] + + +def _set_script(*items: Any) -> _FakeScript: + script = _FakeScript(items) + _current_script[0] = script + return script + + +def _get_script() -> _FakeScript: + script = _current_script[0] + assert script is not None, "Test must call _set_script(...) before running code." + return script + + +class _FakeMonty: + def __init__( + self, + code: str, + *, + script_name: str, + type_check: bool, + type_check_stubs: str | None, + ) -> None: + self.code = code + self.script_name = script_name + self.type_check = type_check + self.type_check_stubs = type_check_stubs + self._script = _get_script() + + def start(self, *, print_callback: Any) -> Any: + while True: + item = self._script.next_item() + if isinstance(item, _PrintAction): + print_callback("stdout", item.text) + continue + return item + + +@pytest.fixture(autouse=True) +def fake_monty_module(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: + """Install a fake ``pydantic_monty`` module for the duration of each test.""" + fake = types.ModuleType("pydantic_monty") + fake.Monty = _FakeMonty # type: ignore[attr-defined] + fake.MontyComplete = _FakeMontyComplete # type: ignore[attr-defined] + fake.FunctionSnapshot = _FakeFunctionSnapshot # type: ignore[attr-defined] + fake.FutureSnapshot = _FakeFutureSnapshot # type: ignore[attr-defined] + fake.NameLookupSnapshot = _FakeNameLookupSnapshot # type: ignore[attr-defined] + + monkeypatch.setitem(sys.modules, "pydantic_monty", fake) + _current_script[0] = None + yield + _current_script[0] = None + + +# --------------------------------------------------------------------------- +# Sample tools used across tests +# --------------------------------------------------------------------------- + + +@tool +def add_tool( + a: Annotated[int, "First addend"], + b: Annotated[int, "Second addend"], +) -> int: + """Add two integers.""" + return a + b + + +@tool +def mul_tool( + a: Annotated[int, "First factor"], + b: Annotated[int, "Second factor"], +) -> int: + """Multiply two integers.""" + return a * b + + +@tool(approval_mode="always_require") +def dangerous_tool(payload: Annotated[str, "Anything"]) -> str: + """A tool that always requires approval.""" + return payload + + +# --------------------------------------------------------------------------- +# MontyExecuteCodeTool tests +# --------------------------------------------------------------------------- + + +def test_tool_construction_defaults() -> None: + monty_tool = MontyExecuteCodeTool() + assert monty_tool.name == "execute_code" + assert monty_tool.approval_mode == "never_require" + assert monty_tool.get_tools() == [] + + +def test_add_remove_clear_tools_round_trip() -> None: + monty_tool = MontyExecuteCodeTool() + + monty_tool.add_tools([add_tool, mul_tool]) + assert [t.name for t in monty_tool.get_tools()] == ["add_tool", "mul_tool"] + + monty_tool.remove_tool("add_tool") + assert [t.name for t in monty_tool.get_tools()] == ["mul_tool"] + + with pytest.raises(KeyError): + monty_tool.remove_tool("missing") + + monty_tool.clear_tools() + assert monty_tool.get_tools() == [] + + +def test_approval_required_tool_gates_execute_code() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + assert monty_tool.approval_mode == "never_require" + + monty_tool.add_tools([dangerous_tool]) + assert monty_tool.approval_mode == "always_require" + + monty_tool.remove_tool("dangerous_tool") + assert monty_tool.approval_mode == "never_require" + + +def test_default_approval_mode_always_require_is_sticky() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add_tool], approval_mode="always_require") + assert monty_tool.approval_mode == "always_require" + + monty_tool.clear_tools() + assert monty_tool.approval_mode == "always_require" + + +def test_dynamic_description_reflects_registered_tools() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + description = monty_tool.description + assert "add_tool" in description + assert "Monty" in description + + monty_tool.add_tools([mul_tool]) + description_updated = monty_tool.description + assert "mul_tool" in description_updated + + +def test_create_run_tool_snapshots_current_state() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add_tool], approval_mode="never_require") + run_tool = monty_tool.create_run_tool() + + assert run_tool is not monty_tool + assert [t.name for t in run_tool.get_tools()] == ["add_tool"] + assert run_tool.approval_mode == monty_tool.approval_mode + + # Mutating the original must not leak into the snapshot. + monty_tool.add_tools([mul_tool]) + assert [t.name for t in run_tool.get_tools()] == ["add_tool"] + + +def test_build_serializable_state_matches_effective_config() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add_tool, dangerous_tool]) + state = monty_tool.build_serializable_state() + assert state["runtime"] == "monty" + assert state["approval_mode"] == "always_require" + assert set(state["tool_names"]) == {"add_tool", "dangerous_tool"} + assert state["workspace_root"] is None + assert state["file_mounts"] == [] + assert state["resource_limits"] is None + + +def test_file_mounts_normalized_and_round_tripped(tmp_path: Path) -> None: + from agent_framework_monty import FileMount + from agent_framework_monty._execute_code_tool import _normalize_mount_path + + host_a = tmp_path / "a" + host_a.mkdir() + host_b = tmp_path / "b" + host_b.mkdir() + + monty_tool = MontyExecuteCodeTool( + file_mounts=[ + str(host_a), # shorthand: same path on both sides + (str(host_b), "/work"), # explicit tuple + FileMount(host_path=host_a, mount_path="/data", mode="read-only"), + ], + ) + + mounts = monty_tool.get_file_mounts() + by_mount = {m.mount_path: m for m in mounts} + + # The shorthand string is normalized through _normalize_mount_path (POSIX-style), + # so on Windows `C:\\...` becomes `/C:/...`. Compare against the same normalizer. + shorthand_key = _normalize_mount_path(str(host_a)) + assert set(by_mount) == {shorthand_key, "/work", "/data"} + assert by_mount["/work"].host_path == host_b.resolve() + assert by_mount["/data"].mode == "read-only" + assert by_mount[shorthand_key].mode == "overlay" # default + + +def test_workspace_root_auto_mounts_at_input(tmp_path: Path) -> None: + monty_tool = MontyExecuteCodeTool(workspace_root=tmp_path) + mounts = monty_tool._effective_mounts() + assert any(m.mount_path == "/input" and m.mode == "read-write" for m in mounts) + + +def test_workspace_root_yields_to_explicit_input_mount(tmp_path: Path) -> None: + from agent_framework_monty import FileMount + + explicit = tmp_path / "explicit" + explicit.mkdir() + monty_tool = MontyExecuteCodeTool( + workspace_root=tmp_path, + file_mounts=[FileMount(host_path=explicit, mount_path="/input", mode="read-only")], + ) + input_mounts = [m for m in monty_tool._effective_mounts() if m.mount_path == "/input"] + assert len(input_mounts) == 1 + assert input_mounts[0].mode == "read-only" + assert input_mounts[0].host_path == explicit.resolve() + + +def test_remove_file_mount_raises_on_missing() -> None: + monty_tool = MontyExecuteCodeTool() + with pytest.raises(KeyError): + monty_tool.remove_file_mount("/never-added") + + +def test_dynamic_description_mentions_filesystem_when_mounts_configured(tmp_path: Path) -> None: + monty_tool = MontyExecuteCodeTool(workspace_root=tmp_path) + description = monty_tool.description + assert "Filesystem access is enabled" in description + assert "/input" in description + + +def test_dynamic_description_default_mentions_no_filesystem() -> None: + monty_tool = MontyExecuteCodeTool() + description = monty_tool.description + assert "Filesystem access is unavailable" in description + + +def test_resource_limits_round_trip() -> None: + monty_tool = MontyExecuteCodeTool(resource_limits={"max_duration_secs": 5.0}) + assert monty_tool.resource_limits == {"max_duration_secs": 5.0} + state = monty_tool.build_serializable_state() + assert state["resource_limits"] == {"max_duration_secs": 5.0} + + +def test_build_instructions_includes_registered_tools() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + instructions = monty_tool.build_instructions(tools_visible_to_model=False) + assert "add_tool" in instructions + assert "execute_code" in instructions + assert "asyncio.gather" in instructions + + +def test_execute_code_filtered_out_when_added_as_tool() -> None: + spurious = FunctionTool( + name="execute_code", + description="should not appear", + func=lambda: None, + ) + monty_tool = MontyExecuteCodeTool(tools=[spurious, add_tool]) + assert [t.name for t in monty_tool.get_tools()] == ["add_tool"] + + +# --------------------------------------------------------------------------- +# _run_code behavior with the fake Monty runtime +# --------------------------------------------------------------------------- + + +async def test_run_code_with_no_tools_returns_default_text() -> None: + _set_script(_FakeMontyComplete(output=None)) + + monty_tool = MontyExecuteCodeTool() + result = await monty_tool._run_code(code="None") + + assert len(result) == 1 + assert isinstance(result[0], Content) + + +async def test_run_code_surfaces_stdout_and_output() -> None: + _set_script(_PrintAction("hello\n"), _FakeMontyComplete(output=42)) + + monty_tool = MontyExecuteCodeTool() + result = await monty_tool._run_code(code="print('hello')") + + text_contents = [c for c in result if c.type == "text"] + assert any("hello" in (c.text or "") for c in text_contents) + assert any( + (c.text or "").strip() and json.loads(c.text or "null") == 42 + for c in text_contents + if (c.text or "").strip().isdigit() + ) + + +async def test_run_code_direct_typed_call_invokes_registered_tool() -> None: + func_snapshot = _FakeFunctionSnapshot( + function_name="add_tool", + call_id=1, + kwargs={"a": 2, "b": 3}, + ) + future_snapshot = _FakeFutureSnapshot(pending_call_ids=[1]) + script = _set_script(func_snapshot, future_snapshot, _FakeMontyComplete(output=None)) + + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + await monty_tool._run_code(code="await add_tool(a=2, b=3)") + + payloads = [payload for _, _, payload in script.resume_log] + assert {"future": ...} in payloads + final_resume = next(p for p in payloads if isinstance(p, dict) and 1 in p) + assert final_resume[1] == {"return_value": 5} + + +async def test_run_code_call_tool_fallback_invokes_registered_tool() -> None: + func_snapshot = _FakeFunctionSnapshot( + function_name="call_tool", + call_id=7, + args=("add_tool",), + kwargs={"a": 4, "b": 8}, + ) + future_snapshot = _FakeFutureSnapshot(pending_call_ids=[7]) + script = _set_script(func_snapshot, future_snapshot, _FakeMontyComplete(output=None)) + + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + await monty_tool._run_code(code="await call_tool('add_tool', a=4, b=8)") + + payloads = [payload for _, _, payload in script.resume_log] + final_resume = next(p for p in payloads if isinstance(p, dict) and 7 in p) + assert final_resume[7] == {"return_value": 12} + + +async def test_run_code_unknown_tool_returns_nameerror_resume() -> None: + func_snapshot = _FakeFunctionSnapshot( + function_name="does_not_exist", + call_id=11, + ) + script = _set_script(func_snapshot, _FakeMontyComplete(output=None)) + + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + await monty_tool._run_code(code="await does_not_exist()") + + payloads = [payload for _, _, payload in script.resume_log] + assert any(isinstance(p, dict) and p.get("exc_type") == "NameError" for p in payloads) + + +async def test_run_code_os_function_is_rejected_with_permissionerror() -> None: + os_snapshot = _FakeFunctionSnapshot( + function_name="os.listdir", + call_id=12, + is_os_function=True, + ) + script = _set_script(os_snapshot, _FakeMontyComplete(output=None)) + + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + await monty_tool._run_code(code="import os; os.listdir('.')") + + payloads = [payload for _, _, payload in script.resume_log] + assert any(isinstance(p, dict) and p.get("exc_type") == "PermissionError" for p in payloads) + + +async def test_when_any_returns_nameerror_now_that_it_is_removed() -> None: + """`when_any` is no longer part of the DSL and should resolve to a NameError.""" + func_snapshot = _FakeFunctionSnapshot( + function_name="when_any", + call_id=99, + args=([{"tool": "add_tool", "kwargs": {"a": 1, "b": 2}}],), + ) + script = _set_script(func_snapshot, _FakeMontyComplete(output=None)) + + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + await monty_tool._run_code(code="await when_any([{'tool': 'add_tool', 'kwargs': {'a': 1, 'b': 2}}])") + + payloads = [payload for _, _, payload in script.resume_log] + assert any(isinstance(p, dict) and p.get("exc_type") == "NameError" for p in payloads) + + +async def test_run_code_call_tool_with_unregistered_name_returns_error() -> None: + func_snapshot = _FakeFunctionSnapshot( + function_name="call_tool", + call_id=20, + args=("missing",), + kwargs={}, + ) + script = _set_script(func_snapshot, _FakeMontyComplete(output=None)) + + monty_tool = MontyExecuteCodeTool(tools=[add_tool]) + await monty_tool._run_code(code="await call_tool('missing')") + + payloads = [payload for _, _, payload in script.resume_log] + assert any( + isinstance(p, dict) and p.get("exc_type") == "ValueError" and "Tool 'missing'" in p.get("message", "") + for p in payloads + ) + + +async def test_run_code_returns_error_content_on_runtime_failure(monkeypatch: pytest.MonkeyPatch) -> None: + class _BoomBridge: + def __init__(self, tool_map: Any, **_: Any) -> None: + pass + + async def run(self, code: str) -> dict[str, Any]: + raise RuntimeError("boom") + + monkeypatch.setattr(execute_code_module, "InlineCodeBridge", _BoomBridge) + + monty_tool = MontyExecuteCodeTool() + result = await monty_tool._run_code(code="x = 1") + assert len(result) == 1 + assert result[0].type == "error" + assert "boom" in (result[0].error_details or "") + + +# --------------------------------------------------------------------------- +# MontyCodeActProvider tests +# --------------------------------------------------------------------------- + + +async def test_provider_injects_execute_code_tool_and_instructions() -> None: + provider = MontyCodeActProvider(tools=[add_tool]) + context = SessionContext(input_messages=[Message(role="user", contents=[Content.from_text("hi")])]) + state: dict[str, Any] = {} + + await provider.before_run(agent=MagicMock(), session=None, context=context, state=state) + + assert state["monty_codeact"]["tool_names"] == ["add_tool"] + assert any("add_tool" in instruction for instruction in context.instructions) + assert len(context.tools) == 1 + assert isinstance(context.tools[0], MontyExecuteCodeTool) + # The injected tool is a per-run snapshot, not the provider's stored copy. + assert context.tools[0] is not provider._execute_code_tool # type: ignore[attr-defined] + + +def test_provider_delegates_tool_management_to_internal_tool() -> None: + provider = MontyCodeActProvider() + provider.add_tools([add_tool, mul_tool]) + assert [t.name for t in provider.get_tools()] == ["add_tool", "mul_tool"] + + provider.remove_tool("add_tool") + assert [t.name for t in provider.get_tools()] == ["mul_tool"] + + provider.clear_tools() + assert provider.get_tools() == [] + + +# --------------------------------------------------------------------------- +# generate_type_stubs - signature smoke test +# --------------------------------------------------------------------------- + + +def test_generate_type_stubs_emits_dsl_and_tool_signatures() -> None: + def custom(x: int, y: str = "z") -> bool: + """Stub-test tool.""" + return True + + stubs = bridge_module.generate_type_stubs({"custom": custom}) + + assert "async def call_tool(name: str, **kwargs: Any) -> Any:" in stubs + assert "async def custom(x: int, y: str = ...) -> bool:" in stubs + assert "when_any" not in stubs + + +def test_generate_type_stubs_preserves_none_and_optional() -> None: + + def nullable_return(x: int) -> None: + """Returns nothing.""" + return + + def optional_param(x: int | None = None) -> bool: # noqa: UP045 - intentional + """Optional via typing.Optional.""" + return x is None + + def union_param(x: int | str | None) -> str: # noqa: UP007 - intentional + """Union with None.""" + return str(x) + + stubs = bridge_module.generate_type_stubs({ + "nullable_return": nullable_return, + "optional_param": optional_param, + "union_param": union_param, + }) + + # ``None`` return must round-trip as None, not Any. + assert "async def nullable_return(x: int) -> None:" in stubs + # ``Optional[X]`` is ``Union[X, None]`` at runtime; preserve None. + assert "async def optional_param(x: int | None = ...) -> bool:" in stubs + # Multi-arm union with None. + assert "async def union_param(x: int | str | None) -> str:" in stubs + + +def test_generate_type_stubs_skips_non_identifier_tool_names() -> None: + """Tool names that are not valid Python identifiers must not be splatted into stub source. + + The model can still reach them via ``call_tool("weird-name", ...)`` at + runtime; they just don't get type-checked stubs. + """ + + def evil(x: int) -> int: + return x + + def normal(x: int) -> int: + return x + + stubs = bridge_module.generate_type_stubs({ + # Hyphens are not valid identifier chars. + "weird-name": evil, + # Newlines in the name would inject arbitrary stub source. + "broken\n pass\nasync def injected": evil, + # Python keywords are valid identifiers per ``str.isidentifier()`` but + # would still produce uncompilable stubs. + "async": evil, + # Real tool that should still appear. + "normal": normal, + }) + + assert "async def normal(x: int) -> int:" in stubs + assert "weird-name" not in stubs + assert "injected" not in stubs + assert "async def async(" not in stubs + + +async def test_invoke_tool_awaits_partial_wrapped_async_method() -> None: + """A FunctionTool callback registered via partial(FunctionTool.invoke, ...) must be awaited. + + Regression for PR #5915 review feedback: relying on ``inspect.iscoroutinefunction`` + to choose between ``await`` and ``asyncio.to_thread`` is fragile for + ``functools.partial`` wrappers (cpython#98590) and would surface the + returned coroutine as a JSON-serialization error instead of the real + tool result. The bridge must always ``await`` entries in ``self.tool_map``. + """ + from functools import partial + + from agent_framework_monty._monty_bridge import InlineCodeBridge + + @tool + def adder(a: Annotated[int, ""], b: Annotated[int, ""]) -> int: + """Add.""" + return a + b + + # Mirrors what _make_tool_callback returns. + cb = partial(adder.invoke, skip_parsing=True) + bridge = InlineCodeBridge({"adder": cb}) + + cid, payload = await bridge._invoke_tool(7, "adder", {"a": 6, "b": 7}) + assert cid == 7 + assert payload == {"return_value": 13}, payload diff --git a/python/packages/monty/tests/monty/test_monty_codeact_integration.py b/python/packages/monty/tests/monty/test_monty_codeact_integration.py new file mode 100644 index 0000000000..2728936c9d --- /dev/null +++ b/python/packages/monty/tests/monty/test_monty_codeact_integration.py @@ -0,0 +1,601 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Integration tests for ``agent_framework_monty`` exercising the real Monty runtime. + +These tests import the real ``pydantic-monty`` package and run actual Python +code through it via :class:`MontyExecuteCodeTool`. They are marked +``@pytest.mark.integration`` and are skipped automatically when +``pydantic_monty`` is unavailable. +""" + +from __future__ import annotations + +import asyncio +import importlib.util +import time +from typing import Annotated, Any +from unittest.mock import MagicMock + +import pytest +from agent_framework import Agent, Content, Message, tool +from agent_framework._sessions import SessionContext + +from agent_framework_monty import MontyCodeActProvider, MontyExecuteCodeTool + + +def _monty_integration_skip_reason() -> str | None: + if importlib.util.find_spec("pydantic_monty") is None: + return "pydantic-monty is not installed." + return None + + +pytestmark = [ + pytest.mark.integration, + pytest.mark.skipif( + _monty_integration_skip_reason() is not None, + reason=_monty_integration_skip_reason() or "Monty integration tests are disabled.", + ), +] + + +# --------------------------------------------------------------------------- +# Sample tools +# --------------------------------------------------------------------------- + + +@tool +def add( + a: Annotated[int, "First addend"], + b: Annotated[int, "Second addend"], +) -> int: + """Return ``a + b``.""" + return a + b + + +@tool +def multiply( + a: Annotated[int, "First factor"], + b: Annotated[int, "Second factor"], +) -> int: + """Return ``a * b``.""" + return a * b + + +@tool +async def async_echo(value: Annotated[str, "Value to echo"]) -> str: + """Return ``value`` after a no-op await.""" + await asyncio.sleep(0) + return value + + +def _async_slow_factory(label: str, delay: float) -> Any: + @tool(name=f"slow_{label}") + async def slow(value: Annotated[int, "Input"]) -> int: + """Sleep asynchronously, then return value untouched.""" + await asyncio.sleep(delay) + return value + + return slow + + +@tool(approval_mode="always_require") +def restricted(payload: Annotated[str, "Any text"]) -> str: + """A tool that always requires approval.""" + return payload + + +def _text_outputs(contents: list[Content]) -> list[str]: + return [c.text or "" for c in contents if c.type == "text"] + + +# --------------------------------------------------------------------------- +# Basic execution +# --------------------------------------------------------------------------- + + +async def test_plain_python_print_round_trips() -> None: + monty_tool = MontyExecuteCodeTool() + result = await monty_tool._run_code(code="print('hello world')") + + texts = _text_outputs(result) + assert any("hello world" in text for text in texts) + + +async def test_last_expression_value_is_returned() -> None: + monty_tool = MontyExecuteCodeTool() + result = await monty_tool._run_code(code="5 + 7") + + texts = _text_outputs(result) + assert any(text.strip() == "12" for text in texts) + + +# --------------------------------------------------------------------------- +# Tool dispatch +# --------------------------------------------------------------------------- + + +async def test_direct_typed_tool_call_invokes_host() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add]) + result = await monty_tool._run_code(code="print(await add(a=2, b=3))") + + texts = _text_outputs(result) + assert any("5" in text for text in texts) + + +async def test_call_tool_fallback_invokes_host() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add]) + result = await monty_tool._run_code(code="print(await call_tool('add', a=4, b=8))") + + texts = _text_outputs(result) + assert any("12" in text for text in texts) + + +async def test_async_host_tool_is_awaited() -> None: + monty_tool = MontyExecuteCodeTool(tools=[async_echo]) + result = await monty_tool._run_code(code="print(await async_echo(value='ping'))") + + texts = _text_outputs(result) + assert any("ping" in text for text in texts) + + +# --------------------------------------------------------------------------- +# Concurrency +# --------------------------------------------------------------------------- + + +async def test_asyncio_gather_fans_out_tool_calls_concurrently() -> None: + """Two async tools dispatched via ``asyncio.gather`` should run on the event loop in parallel. + + Sync tools cannot fan out (FunctionTool.invoke runs them inline on the event loop), + so this test uses async host tools to verify the bridge's gather pipeline does + not introduce extra serialization. + """ + slow_a = _async_slow_factory("a", delay=0.25) + slow_b = _async_slow_factory("b", delay=0.25) + monty_tool = MontyExecuteCodeTool(tools=[slow_a, slow_b]) + + code = """ +results = await asyncio.gather(slow_a(value=1), slow_b(value=2)) +print(results) +""" + + start = time.perf_counter() + result = await monty_tool._run_code(code=code) + elapsed = time.perf_counter() - start + + texts = _text_outputs(result) + assert any("[1, 2]" in text for text in texts) + # Allow some scheduling slack but verify it's noticeably less than sequential (~0.5s). + assert elapsed < 0.45, f"Expected concurrent execution; took {elapsed:.3f}s" + + +# --------------------------------------------------------------------------- +# Sandbox safety + type checking +# --------------------------------------------------------------------------- + + +async def test_type_check_rejects_wrong_argument_type() -> None: + invocation_count = {"count": 0} + + @tool + def typed_add( + a: Annotated[int, "First"], + b: Annotated[int, "Second"], + ) -> int: + """Add two ints; records invocations.""" + invocation_count["count"] += 1 + return a + b + + monty_tool = MontyExecuteCodeTool(tools=[typed_add]) + result = await monty_tool._run_code(code="print(await typed_add(a='not an int', b=3))") + + texts = _text_outputs(result) + errors = [c for c in result if c.type == "error"] + # Either ty raises and surfaces as an error Content, or Monty reports the typing error in stdout. + assert errors or any("type" in text.lower() or "monty" in text.lower() for text in texts) + assert invocation_count["count"] == 0 + + +async def test_os_calls_are_blocked() -> None: + monty_tool = MontyExecuteCodeTool() + code = """ +try: + import os + os.listdir('/') + print('LEAKED') +except PermissionError as exc: + print('blocked:', exc) +except Exception as exc: + print('other:', type(exc).__name__) +""" + result = await monty_tool._run_code(code=code) + texts = _text_outputs(result) + assert not any("LEAKED" in text for text in texts) + assert any("blocked" in text or "PermissionError" in text or "other" in text for text in texts) + + +async def test_unknown_tool_call_returns_clean_error() -> None: + monty_tool = MontyExecuteCodeTool(tools=[add]) + code = """ +try: + await call_tool('missing') +except Exception as exc: + print('err:', type(exc).__name__, str(exc)) +""" + result = await monty_tool._run_code(code=code) + texts = _text_outputs(result) + assert any("missing" in text for text in texts) + + +# --------------------------------------------------------------------------- +# Print capture +# --------------------------------------------------------------------------- + + +async def test_print_truncation_caps_output() -> None: + monty_tool = MontyExecuteCodeTool() + # Emit more than MAX_PRINT_OUTPUT_CHARS bytes of output. + code = """ +for _ in range(2000): + print('X' * 64) +""" + result = await monty_tool._run_code(code=code) + texts = _text_outputs(result) + combined = "\n".join(texts) + assert len(combined) <= 9000 # MAX_PRINT_OUTPUT_CHARS=8192 plus a small truncation marker + assert "[stdout truncated]" in combined + + +# --------------------------------------------------------------------------- +# Filesystem (workspace_root, file_mounts, output capture, resource limits) +# --------------------------------------------------------------------------- + + +async def test_workspace_root_reads_seed_files_from_host(tmp_path: Any) -> None: + seed = tmp_path / "seed.txt" + seed.write_text("hello from host", encoding="utf-8") + monty_tool = MontyExecuteCodeTool(workspace_root=tmp_path) + + code = """ +import pathlib +data = pathlib.Path('/input/seed.txt').read_text() +print(data) +""" + result = await monty_tool._run_code(code=code) + texts = _text_outputs(result) + assert any("hello from host" in text for text in texts) + + +async def test_workspace_root_writes_are_captured_as_content(tmp_path: Any) -> None: + monty_tool = MontyExecuteCodeTool(workspace_root=tmp_path) + + code = """ +import pathlib +pathlib.Path('/input/report.txt').write_text('result-payload') +print('wrote report') +""" + result = await monty_tool._run_code(code=code) + data_contents = [c for c in result if c.type == "data"] + assert len(data_contents) == 1, [c.type for c in result] + written = data_contents[0] + # Content.from_data stores bytes as a base64-encoded data: URI. + import base64 + + assert written.uri is not None + payload = written.uri.split(",", 1)[1] + assert base64.b64decode(payload) == b"result-payload" + assert (written.additional_properties or {}).get("path") == "/input/report.txt" + # And the file actually landed on the host filesystem (read-write mode). + assert (tmp_path / "report.txt").read_text() == "result-payload" + + +async def test_read_only_mount_writes_are_rejected_and_not_captured(tmp_path: Any) -> None: + from agent_framework_monty import FileMount + + seed = tmp_path / "seed.txt" + seed.write_text("ro-content", encoding="utf-8") + + monty_tool = MontyExecuteCodeTool( + file_mounts=[FileMount(host_path=tmp_path, mount_path="/ro", mode="read-only")], + ) + + code = """ +import pathlib +print(pathlib.Path('/ro/seed.txt').read_text()) +try: + pathlib.Path('/ro/should-not-exist.txt').write_text('nope') + print('LEAKED') +except Exception as exc: + print('write blocked:', type(exc).__name__) +""" + result = await monty_tool._run_code(code=code) + texts = _text_outputs(result) + assert any("ro-content" in t for t in texts) + assert not any("LEAKED" in t for t in texts) + # No write went to host; no captured Content for the rejected write. + assert not (tmp_path / "should-not-exist.txt").exists() + assert not any(c.type == "data" for c in result) + + +async def test_overlay_mount_writes_do_not_persist_to_host(tmp_path: Any) -> None: + from agent_framework_monty import FileMount + + monty_tool = MontyExecuteCodeTool( + file_mounts=[FileMount(host_path=tmp_path, mount_path="/overlay", mode="overlay")], + ) + + code = """ +import pathlib +pathlib.Path('/overlay/scratch.txt').write_text('overlay-only') +print('wrote') +""" + result = await monty_tool._run_code(code=code) + assert any("wrote" in t for t in _text_outputs(result)) + # Overlay writes stay in-memory: nothing on host, nothing captured. + assert not (tmp_path / "scratch.txt").exists() + assert not any(c.type == "data" for c in result) + + +async def test_resource_limit_short_duration_aborts_long_loop() -> None: + # Cap CPU time hard; a busy loop should be killed before it can print 'done'. + monty_tool = MontyExecuteCodeTool(resource_limits={"max_duration_secs": 0.2}) + + code = """ +total = 0 +for i in range(10_000_000): + total += i +print('done', total) +""" + result = await monty_tool._run_code(code=code) + # Result is either an error Content (timeout surfaces as RuntimeError) or + # truncated stdout without the 'done' marker. + texts = _text_outputs(result) + assert not any("done" in t for t in texts), texts + + +# --------------------------------------------------------------------------- +# Symlink escape regression (MSRC-style) +# --------------------------------------------------------------------------- + + +def _symlinks_supported(tmp: Any) -> bool: + """Return True if the current platform/environment supports symlinks. + + Mirrors python/packages/core/tests/core/test_skills.py so the symlink + regression tests are skipped on restricted Windows CI runners instead of + failing on ``OSError`` / ``NotImplementedError`` during creation. + """ + test_target = tmp / "_symlink_test_target" + test_link = tmp / "_symlink_test_link" + try: + test_target.write_text("test", encoding="utf-8") + test_link.symlink_to(test_target) + return True + except (OSError, NotImplementedError): + return False + finally: + test_link.unlink(missing_ok=True) + test_target.unlink(missing_ok=True) + + +async def test_symlinks_inside_workspace_are_not_followed_by_runtime(tmp_path: Any) -> None: + """A pre-existing symlink in workspace_root must NOT let sandbox code read its target. + + Monty's mount layer enforces this (PermissionError at the OS bridge), but we + pin the behavior here so any future change to the OS dispatch path is + detected. + """ + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside_secret.txt" + outside.write_text("SECRET_OUTSIDE_WORKSPACE", encoding="utf-8") + (workspace / "leak.txt").symlink_to(outside) + + monty_tool = MontyExecuteCodeTool(workspace_root=workspace) + code = """ +import pathlib +try: + print('read:', pathlib.Path('/input/leak.txt').read_text()) +except PermissionError as exc: + print('blocked:', exc) +except Exception as exc: + print('other:', type(exc).__name__, exc) +""" + result = await monty_tool._run_code(code=code) + texts = _text_outputs(result) + assert not any("SECRET_OUTSIDE_WORKSPACE" in t for t in texts), texts + assert any("blocked" in t or "PermissionError" in t or "other" in t for t in texts), texts + + +async def test_post_capture_skips_symlinks_pointing_outside_workspace(tmp_path: Any) -> None: + """File capture must NOT read through a symlink that points outside the mount. + + Reproduces the MSRC-reported Hyperlight pattern in Monty's post-execution + file-capture path: an attacker-placed ``workspace/leak.txt -> /outside/secret`` + must not be returned as Content. + """ + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside_secret.txt" + outside.write_text("SECRET_OUTSIDE_WORKSPACE", encoding="utf-8") + (workspace / "leak.txt").symlink_to(outside) + outside_dir = tmp_path / "outside_dir" + outside_dir.mkdir() + (outside_dir / "deep.txt").write_text("DEEP_SECRET", encoding="utf-8") + (workspace / "leak_dir").symlink_to(outside_dir) + + monty_tool = MontyExecuteCodeTool(workspace_root=workspace) + # Run trivial code so the post-execution scan fires. + result = await monty_tool._run_code(code="print('ran')") + + # Inspect the URIs of any returned data Content items. + import base64 + + leaked_paths: list[str] = [] + leaked_bodies: list[bytes] = [] + for content in result: + if content.type != "data" or not content.uri: + continue + payload = content.uri.split(",", 1)[1] if "," in content.uri else "" + try: + body = base64.b64decode(payload) + except Exception: # noqa: BLE001 + body = b"" + leaked_bodies.append(body) + leaked_paths.append((content.additional_properties or {}).get("path", "")) + + assert not any(b"SECRET_OUTSIDE_WORKSPACE" in body for body in leaked_bodies), ( + "Symlink file outside workspace was captured: " + repr(leaked_paths) + ) + assert not any(b"DEEP_SECRET" in body for body in leaked_bodies), ( + "Symlinked directory escape was captured: " + repr(leaked_paths) + ) + + +async def test_post_capture_still_returns_real_writes_when_symlinks_present(tmp_path: Any) -> None: + """The symlink-skipping logic must not regress capture of legitimate sandbox writes.""" + if not _symlinks_supported(tmp_path): + pytest.skip("Symlinks not supported on this platform/environment") + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside_secret.txt" + outside.write_text("SHOULD_NEVER_LEAK", encoding="utf-8") + (workspace / "leak.txt").symlink_to(outside) + + monty_tool = MontyExecuteCodeTool(workspace_root=workspace) + code = """ +import pathlib +pathlib.Path('/input/report.txt').write_text('legit-output') +print('wrote') +""" + result = await monty_tool._run_code(code=code) + import base64 + + data_items = [c for c in result if c.type == "data" and c.uri] + # Exactly one new file should be captured: report.txt. + assert len(data_items) == 1, [(c.additional_properties or {}).get("path") for c in data_items] + item = data_items[0] + assert (item.additional_properties or {}).get("path") == "/input/report.txt" + payload = item.uri.split(",", 1)[1] if item.uri and "," in item.uri else "" + assert base64.b64decode(payload) == b"legit-output" + + +# --------------------------------------------------------------------------- +# Provider + approval gating +# --------------------------------------------------------------------------- + + +async def test_provider_run_tool_executes_real_monty_end_to_end() -> None: + provider = MontyCodeActProvider(tools=[add]) + context = SessionContext(input_messages=[Message(role="user", contents=[Content.from_text("hi")])]) + state: dict[str, Any] = {} + + await provider.before_run(agent=MagicMock(), session=None, context=context, state=state) + + run_tool = context.tools[0] + assert isinstance(run_tool, MontyExecuteCodeTool) + + result = await run_tool._run_code(code="print(await add(a=10, b=32))") + texts = _text_outputs(result) + assert any("42" in text for text in texts) + + +async def test_approval_required_tool_gates_execute_code_end_to_end() -> None: + provider = MontyCodeActProvider(tools=[restricted]) + context = SessionContext(input_messages=[Message(role="user", contents=[Content.from_text("hi")])]) + state: dict[str, Any] = {} + + await provider.before_run(agent=MagicMock(), session=None, context=context, state=state) + run_tool = context.tools[0] + assert isinstance(run_tool, MontyExecuteCodeTool) + assert run_tool.approval_mode == "always_require" + assert state["monty_codeact"]["approval_mode"] == "always_require" + + +# --------------------------------------------------------------------------- +# End-to-end Agent run with a fake chat client +# --------------------------------------------------------------------------- + + +async def test_agent_runs_monty_codeact_end_to_end() -> None: + """A fake chat client emits one execute_code tool call; Monty runs it end-to-end.""" + from collections.abc import Awaitable, Mapping, MutableSequence + + from agent_framework import ( + BaseChatClient, + ChatResponse, + ChatResponseUpdate, + FunctionInvocationLayer, + ResponseStream, + ) + + class _FakeCodeActChatClient(FunctionInvocationLayer[Any], BaseChatClient[Any]): + def __init__(self) -> None: + FunctionInvocationLayer.__init__(self) + BaseChatClient.__init__(self) + self.call_count = 0 + + def _inner_get_response( + self, + *, + messages: MutableSequence[Message], + stream: bool, + options: Mapping[str, Any], + **kwargs: Any, + ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: + if stream: + raise AssertionError("Streaming is not used in this integration test.") + + async def _get_response() -> ChatResponse: + self.call_count += 1 + + if self.call_count == 1: + return ChatResponse( + messages=Message( + role="assistant", + contents=[ + Content.from_function_call( + call_id="execute_code_call", + name="execute_code", + arguments={"code": "print(await add(a=6, b=7))"}, + ) + ], + ) + ) + + function_results = [ + content for message in messages for content in message.contents if content.type == "function_result" + ] + assert len(function_results) == 1 + + result_content = function_results[0] + result_text = "" + if isinstance(result_content.result, list): + for item in result_content.result: + text = getattr(item, "text", None) + if text: + result_text += text + else: + result_text = str(result_content.result or "") + + return ChatResponse( + messages=Message( + role="assistant", + contents=[f"answer: {result_text.strip() or 'none'}"], + ) + ) + + return _get_response() + + client = _FakeCodeActChatClient() + provider = MontyCodeActProvider(tools=[add]) + agent = Agent(client=client, context_providers=[provider]) + + response = await agent.run("Add 6 and 7 inside execute_code.") + assert "13" in (response.text or "") + assert client.call_count == 2 diff --git a/python/pyproject.toml b/python/pyproject.toml index c1ad252cba..7716598ded 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -88,6 +88,7 @@ agent-framework-github-copilot = { workspace = true } agent-framework-hyperlight = { workspace = true } agent-framework-lab = { workspace = true } agent-framework-mem0 = { workspace = true } +agent-framework-monty = { workspace = true } agent-framework-ollama = { workspace = true } agent-framework-openai = { workspace = true } agent-framework-orchestrations = { workspace = true } diff --git a/python/samples/02-agents/context_providers/code_act/README.md b/python/samples/02-agents/context_providers/code_act/README.md index 264123d70f..4e52c18da4 100644 --- a/python/samples/02-agents/context_providers/code_act/README.md +++ b/python/samples/02-agents/context_providers/code_act/README.md @@ -1,20 +1,31 @@ -# Hyperlight CodeAct context provider +# CodeAct context providers -Demonstrates the provider-owned [Hyperlight](https://github.com/hyperlight-dev/hyperlight) -CodeAct flow. `HyperlightCodeActProvider` injects an `execute_code` tool into the -agent and keeps the registered sandbox tools (`compute`, `fetch_data`) hidden -from the model — the model must call them from inside the sandbox using -`call_tool(...)`. +Demonstrates the provider-owned CodeAct flow with two backends: + +| File | Backend | Notes | +|------|---------|-------| +| [`code_act.py`](code_act.py) | [Hyperlight](https://github.com/hyperlight-dev/hyperlight) WASM sandbox via `HyperlightCodeActProvider` | Hardened sandbox with WASM isolation; sandbox tools called via `call_tool(...)`. | +| [`monty_code_act.py`](monty_code_act.py) | [Monty](https://github.com/pydantic/monty) Rust-based Python interpreter via `MontyCodeActProvider` (alpha) | Cross-platform pure interpreter; sandbox tools can be called as typed async functions (`await compute(...)`) or via `call_tool(...)`. | + +Both providers inject an `execute_code` tool into the agent and keep the +registered sandbox tools (`compute`, `fetch_data`) hidden from the model — the +model invokes them from inside the sandbox. ## Installation ```bash -pip install agent-framework agent-framework-hyperlight --pre +pip install agent-framework agent-framework-hyperlight --pre # Hyperlight sample +pip install agent-framework agent-framework-monty --pre # Monty sample ``` > The Hyperlight Wasm backend is currently published only for `linux/x86_64` and > `win32/AMD64` with Python `<3.14`. On other platforms `execute_code` will fail > at runtime when it tries to create the sandbox. +> +> Monty is cross-platform and has no hypervisor/WASM backend dependency, but it +> interprets a Python subset (e.g. `os`/network/subprocess access is blocked). +> `agent-framework-monty` is an alpha package and is not yet part of +> `agent-framework[all]`; install it explicitly with `--pre`. ## Prerequisites @@ -25,7 +36,8 @@ pip install agent-framework agent-framework-hyperlight --pre ## Run ```bash -python code_act.py +python code_act.py # Hyperlight +python monty_code_act.py # Monty ``` -See [`code_act.py`](code_act.py) for the full annotated example. +See the source files for the full annotated examples. diff --git a/python/samples/02-agents/context_providers/code_act/monty_code_act.py b/python/samples/02-agents/context_providers/code_act/monty_code_act.py new file mode 100644 index 0000000000..862c92c1d7 --- /dev/null +++ b/python/samples/02-agents/context_providers/code_act/monty_code_act.py @@ -0,0 +1,201 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import asyncio +import logging +import os +from collections.abc import Awaitable, Callable +from typing import Annotated, Any, Literal + +from agent_framework import Agent, FunctionInvocationContext, function_middleware, tool +from agent_framework.foundry import FoundryChatClient +from agent_framework_monty import MontyCodeActProvider +from azure.identity import AzureCliCredential +from dotenv import load_dotenv + +"""This sample demonstrates the provider-owned Monty CodeAct flow. + +The sample keeps `compute` and `fetch_data` off the direct agent tool surface and +registers them only with `MontyCodeActProvider`. The model therefore sees a +single `execute_code` tool and calls the provider-owned tools from inside the +sandbox - either as typed async functions (`await compute(...)`) or via the +generic `call_tool(...)` fallback. + +`MontyCodeActProvider` uses [pydantic-monty](https://github.com/pydantic/monty), +a Rust-based Python interpreter, so it runs cross-platform with no +hypervisor/WASM backend dependency. + +Note: `agent-framework-monty` is an alpha package and is not yet part of +`agent-framework[all]`. Install it explicitly with: + + pip install agent-framework agent-framework-monty --pre + +It is imported as `agent_framework_monty` (no lazy-loading namespace yet). +""" + +load_dotenv() + +_CYAN = "\033[36m" +_YELLOW = "\033[33m" +_GREEN = "\033[32m" +_DIM = "\033[2m" +_RESET = "\033[0m" + + +class _ColoredFormatter(logging.Formatter): + """Dim logger output so it does not compete with sample prints.""" + + def format(self, record: logging.LogRecord) -> str: + return f"{_DIM}{super().format(record)}{_RESET}" + + +logging.basicConfig(level=logging.WARNING) +logging.getLogger().handlers[0].setFormatter( + _ColoredFormatter("[%(asctime)s] %(levelname)s: %(message)s"), +) + + +@function_middleware +async def log_function_calls( + context: FunctionInvocationContext, + call_next: Callable[[], Awaitable[None]], +) -> None: + """Log tool calls, including readable execute_code blocks.""" + import time + + function_name = context.function.name + arguments = context.arguments if isinstance(context.arguments, dict) else {} + + if function_name == "execute_code" and "code" in arguments: + print(f"\n{_YELLOW}{'─' * 60}") + print("▶ execute_code") + print(f"{'─' * 60}{_RESET}") + print(arguments["code"]) + print(f"{_YELLOW}{'─' * 60}{_RESET}") + else: + pairs = ", ".join(f"{name}={value!r}" for name, value in arguments.items()) + print(f"\n{_YELLOW}▶ {function_name}({pairs}){_RESET}") + + start = time.perf_counter() + await call_next() + elapsed = time.perf_counter() - start + + result = context.result + if function_name == "execute_code" and isinstance(result, list): + for output in result: + if output.type == "text" and output.text: + print(f"{_GREEN}stdout:\n{output.text}{_RESET}") + elif output.type == "error" and output.error_details: + print(f"{_YELLOW}stderr:\n{output.error_details}{_RESET}") + else: + print(f"{_YELLOW}◀ {function_name} → {result!r}{_RESET}") + + print(f"{_DIM} ({elapsed:.4f}s){_RESET}") + + +@tool(approval_mode="never_require") +def compute( + operation: Annotated[ + Literal["add", "subtract", "multiply", "divide"], + "Math operation: add, subtract, multiply, or divide.", + ], + a: Annotated[float, "First numeric operand."], + b: Annotated[float, "Second numeric operand."], +) -> float: + """Perform a math operation for sandboxed code.""" + operations = { + "add": a + b, + "subtract": a - b, + "multiply": a * b, + "divide": a / b if b else float("inf"), + } + return operations[operation] + + +@tool(approval_mode="never_require") +async def fetch_data( + table: Annotated[str, "Name of the simulated table to query."], +) -> list[dict[str, Any]]: + """Fetch records from a named table.""" + await asyncio.sleep(0.5) + data: dict[str, list[dict[str, Any]]] = { + "users": [ + {"id": 1, "name": "Alice", "role": "admin"}, + {"id": 2, "name": "Bob", "role": "user"}, + {"id": 3, "name": "Charlie", "role": "admin"}, + ], + "products": [ + {"id": 101, "name": "Widget", "price": 9.99}, + {"id": 102, "name": "Gadget", "price": 19.99}, + ], + } + return data.get(table, []) + + +async def main() -> None: + """Run the provider-owned Monty CodeAct sample.""" + # 1. Create the Monty-backed provider and register sandbox tools on it. + codeact = MontyCodeActProvider( + tools=[compute, fetch_data], + approval_mode="never_require", + ) + + # 2. Create the client and the agent. + agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], + credential=AzureCliCredential(), + ), + name="MontyCodeActProviderAgent", + instructions="You are a helpful assistant.", + context_providers=[codeact], + middleware=[log_function_calls], + ) + + # 3. Run a request that should use execute_code plus provider-owned tools. + query = ( + "Fetch all users, find admins, multiply 7*(3*2), and print the users, " + "admins, and multiplication result. Use a single execute_code call. " + "You may call the registered tools directly as typed async functions " + "(`await compute(operation='multiply', a=7, b=6)`) or via " + "`call_tool('compute', ...)`." + ) + print(f"{_CYAN}{'=' * 60}") + print("Monty CodeAct provider sample") + print(f"{'=' * 60}{_RESET}") + print(f"{_CYAN}User: {query}{_RESET}") + result = await agent.run(query) + print(f"{_CYAN}Agent: {result.text}{_RESET}") + + +""" +Sample output (shape only): + +============================================================ +Monty CodeAct provider sample +============================================================ +User: Fetch all users, find admins, multiply 7*(3*2), ... + +──────────────────────────────────────────────────────────── +▶ execute_code +──────────────────────────────────────────────────────────── +users = await fetch_data(table="users") +admins = [u for u in users if u["role"] == "admin"] +result = await compute(operation="multiply", a=7, b=6) +print("Users:", users) +print("Admins:", admins) +print("7 * 6 =", result) +──────────────────────────────────────────────────────────── +stdout: +Users: [...] +Admins: [...] +7 * 6 = 42.0 + (0.5xxx s) +Agent: ... +""" + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/tools/monty_code_interpreter/README.md b/python/samples/02-agents/tools/monty_code_interpreter/README.md new file mode 100644 index 0000000000..d2797077d6 --- /dev/null +++ b/python/samples/02-agents/tools/monty_code_interpreter/README.md @@ -0,0 +1,40 @@ +# Monty local code interpreter + +Demonstrates the standalone [Monty](https://github.com/pydantic/monty) +`MontyExecuteCodeTool` — a sandboxed local code interpreter that the agent can +invoke directly. Two patterns are shown: + +| File | Pattern | +|------|---------| +| [`monty_code_interpreter.py`](monty_code_interpreter.py) | **Standalone tool** — `MontyExecuteCodeTool` is added to the agent tool list and self-describes its sandbox tools, so no extra agent instructions are needed. Best for quick prototyping. | +| [`monty_code_interpreter_manual_wiring.py`](monty_code_interpreter_manual_wiring.py) | **Manual static wiring** — sandbox tools and CodeAct instructions are built once and passed to the `Agent` constructor alongside a direct-only tool (`send_email`). Best when the tool set is fixed for the agent's lifetime. | + +For the recommended provider-driven pattern (with dynamic tool / capability +management), see +[`../../context_providers/code_act/`](../../context_providers/code_act/). + +## Installation + +```bash +pip install agent-framework agent-framework-monty --pre +``` + +> `agent-framework-monty` is an alpha package and is not yet part of +> `agent-framework[all]`. The `--pre` flag is required. +> +> Monty is cross-platform and has no hypervisor/WASM backend dependency. +> Inside the sandbox, OS / filesystem / network calls are blocked +> (`PermissionError`); registered host tools retain full Python access. + +## Prerequisites + +- An Azure AI Foundry project endpoint (`FOUNDRY_PROJECT_ENDPOINT`) +- A deployed model (`FOUNDRY_MODEL`) +- Azure CLI authenticated (`az login`) + +## Run + +```bash +python monty_code_interpreter.py +python monty_code_interpreter_manual_wiring.py +``` diff --git a/python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter.py b/python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter.py new file mode 100644 index 0000000000..dc9016b915 --- /dev/null +++ b/python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter.py @@ -0,0 +1,114 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import asyncio +import os +from typing import Annotated, Any, Literal + +from agent_framework import Agent, tool +from agent_framework.foundry import FoundryChatClient +from agent_framework_monty import MontyExecuteCodeTool +from azure.identity import AzureCliCredential +from dotenv import load_dotenv + +"""This sample demonstrates the standalone Monty execute_code tool. + +The sample adds `MontyExecuteCodeTool` directly to the agent. The tool's own +description advertises the registered sandbox tools (as typed async functions +and via `call_tool(...)`) plus the Monty DSL, so no extra CodeAct-specific +agent instructions are required. + +Note: `agent-framework-monty` is an alpha package and is not yet part of +`agent-framework[all]`. Install it explicitly with: + + pip install agent-framework agent-framework-monty --pre +""" + +load_dotenv() + + +@tool(approval_mode="never_require") +def compute( + operation: Annotated[ + Literal["add", "subtract", "multiply", "divide"], + "Math operation: add, subtract, multiply, or divide.", + ], + a: Annotated[float, "First numeric operand."], + b: Annotated[float, "Second numeric operand."], +) -> float: + """Perform a math operation used by sandboxed code.""" + operations = { + "add": a + b, + "subtract": a - b, + "multiply": a * b, + "divide": a / b if b else float("inf"), + } + return operations[operation] + + +@tool(approval_mode="never_require") +def fetch_data( + table: Annotated[str, "Name of the simulated table to query."], +) -> list[dict[str, Any]]: + """Fetch simulated records from a named table.""" + data: dict[str, list[dict[str, Any]]] = { + "users": [ + {"id": 1, "name": "Alice", "role": "admin"}, + {"id": 2, "name": "Bob", "role": "user"}, + {"id": 3, "name": "Charlie", "role": "admin"}, + ], + "products": [ + {"id": 101, "name": "Widget", "price": 9.99}, + {"id": 102, "name": "Gadget", "price": 19.99}, + ], + } + return data.get(table, []) + + +async def main() -> None: + """Run the standalone Monty execute_code sample.""" + # 1. Create the packaged execute_code tool and register sandbox tools on it. + execute_code = MontyExecuteCodeTool( + tools=[compute, fetch_data], + approval_mode="never_require", + ) + + # 2. Create the client and the agent. + agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], + credential=AzureCliCredential(), + ), + name="MontyExecuteCodeToolAgent", + instructions="You are a helpful assistant.", + tools=execute_code, + ) + + # 3. Run one request through the direct-tool surface. + print("=" * 60) + print("Monty execute_code tool sample") + print("=" * 60) + query = ( + "Fetch all users, find admins, multiply 6*7, and print the users, admins, " + "and multiplication result. Use one execute_code call." + ) + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result.text}") + + +""" +Sample output (shape only): + +============================================================ +Monty execute_code tool sample +============================================================ +User: Fetch all users, find admins, multiply 6*7, ... +Agent: ... +""" + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter_manual_wiring.py b/python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter_manual_wiring.py new file mode 100644 index 0000000000..104a256a25 --- /dev/null +++ b/python/samples/02-agents/tools/monty_code_interpreter/monty_code_interpreter_manual_wiring.py @@ -0,0 +1,136 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import asyncio +import os +from typing import Annotated, Any, Literal + +from agent_framework import Agent, tool +from agent_framework.foundry import FoundryChatClient +from agent_framework_monty import MontyExecuteCodeTool +from azure.identity import AzureCliCredential +from dotenv import load_dotenv + +"""This sample demonstrates manual static wiring of Monty CodeAct without a provider. + +Instead of using `MontyCodeActProvider` with `context_providers=`, this sample +creates a `MontyExecuteCodeTool` directly, extracts its CodeAct instructions +once, and passes both to the `Agent` constructor at build time. + +This avoids the per-run provider lifecycle (`before_run` / `after_run`) and is +well-suited when the tool registry is fixed for the agent's lifetime. The +tradeoff is that dynamic tool changes between runs are not supported - any +mutations to the tool would not update the agent's instructions automatically. + +Note: `agent-framework-monty` is an alpha package and is not yet part of +`agent-framework[all]`. Install it explicitly with: + + pip install agent-framework agent-framework-monty --pre +""" + +load_dotenv() + + +@tool(approval_mode="never_require") +def compute( + operation: Annotated[ + Literal["add", "subtract", "multiply", "divide"], + "Math operation: add, subtract, multiply, or divide.", + ], + a: Annotated[float, "First numeric operand."], + b: Annotated[float, "Second numeric operand."], +) -> float: + """Perform a math operation used by sandboxed code.""" + operations = { + "add": a + b, + "subtract": a - b, + "multiply": a * b, + "divide": a / b if b else float("inf"), + } + return operations[operation] + + +@tool(approval_mode="never_require") +def fetch_data( + table: Annotated[str, "Name of the simulated table to query."], +) -> list[dict[str, Any]]: + """Fetch simulated records from a named table.""" + data: dict[str, list[dict[str, Any]]] = { + "users": [ + {"id": 1, "name": "Alice", "role": "admin"}, + {"id": 2, "name": "Bob", "role": "user"}, + {"id": 3, "name": "Charlie", "role": "admin"}, + ], + "products": [ + {"id": 101, "name": "Widget", "price": 9.99}, + {"id": 102, "name": "Gadget", "price": 19.99}, + ], + } + return data.get(table, []) + + +@tool(approval_mode="never_require") +def send_email( + to: Annotated[str, "Recipient email address."], + subject: Annotated[str, "Email subject line."], + body: Annotated[str, "Email body text."], +) -> str: + """Simulate sending an email (direct-only tool, not available inside the sandbox).""" + return f"Email sent to {to}: {subject}" + + +async def main() -> None: + """Run the manual static-wiring Monty sample.""" + # 1. Create the execute_code tool and register sandbox tools on it. + execute_code = MontyExecuteCodeTool( + tools=[compute, fetch_data], + approval_mode="never_require", + ) + + # 2. Build CodeAct instructions once. Setting tools_visible_to_model=False + # tells the instructions builder that sandbox tools are not in the agent's + # direct tool list, so the model must call them inside execute_code. + codeact_instructions = execute_code.build_instructions(tools_visible_to_model=False) + + # 3. Create the client and the agent with everything wired at construction time. + # - send_email is a direct-only tool (not available inside the sandbox). + # - execute_code carries sandbox tools (compute, fetch_data) for Monty. + agent = Agent( + client=FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["FOUNDRY_MODEL"], + credential=AzureCliCredential(), + ), + name="MontyManualWiringAgent", + instructions=f"You are a helpful assistant.\n\n{codeact_instructions}", + tools=[send_email, execute_code], + ) + + # 4. Run a request that exercises both the sandbox and the direct tool. + print("=" * 60) + print("Manual static-wiring Monty CodeAct sample") + print("=" * 60) + query = ( + "Fetch all users, find admins, multiply 6*7, and print the users, admins, " + "and multiplication result. Use one execute_code call. " + "Then send an email to admin@example.com summarising the results." + ) + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result.text}") + + +""" +Sample output (shape only): + +============================================================ +Manual static-wiring Monty CodeAct sample +============================================================ +User: Fetch all users, find admins, multiply 6*7, ... +Agent: ... +""" + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/04-hosting/foundry-hosted-agents/README.md b/python/samples/04-hosting/foundry-hosted-agents/README.md index bb55657564..ebb0741892 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/README.md @@ -18,7 +18,8 @@ This directory contains samples that demonstrate how to use hosted [Agent Framew | 8 | [Azure AI Search RAG](responses/08_azure_search_rag/) | An agent with Retrieval Augmented Generation (RAG) capabilities backed by Azure AI Search, grounding answers in documents indexed in a pre-provisioned search index. | | 9 | [Foundry Skills](responses/09_foundry_skills/) | An agent that uploads `SKILL.md` files to the Foundry Skills REST API and downloads them at startup, decoupling tone/policy guidelines from agent code. | | 10 | [Foundry Memory](responses/10_foundry_memory/) | An agent with persistent semantic memory backed by an Azure AI Foundry Memory Store, using `FoundryMemoryProvider` to remember user facts across sessions. | -| 11 | [Using deployed agent](responses/using_deployed_agent.py) | A sample demonstrating how to invoke an agent that has already been deployed to Foundry, showing how to interact with a hosted agent in code. | +| 11 | [Monty CodeAct](responses/11_monty_codeact/) | An agent with a Monty-backed CodeAct context provider, exposing a single `execute_code` tool that runs Python in a [pydantic-monty](https://github.com/pydantic/monty) interpreter and invokes typed host tools (`compute`, `fetch_data`) from inside the sandbox. Uses the alpha `agent-framework-monty` package. | +| 12 | [Using deployed agent](responses/using_deployed_agent.py) | A sample demonstrating how to invoke an agent that has already been deployed to Foundry, showing how to interact with a hosted agent in code. | ### Invocations API diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/Dockerfile b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/Dockerfile new file mode 100644 index 0000000000..514bc9a0d0 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-slim + +# Bring in the `uv` binary from a pinned Astral image. Update this tag intentionally; +# `latest` would make rebuilds non-deterministic. +COPY --from=ghcr.io/astral-sh/uv:0.11.6 /uv /uvx /usr/local/bin/ + +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + UV_PROJECT_ENVIRONMENT=/app/.venv \ + PATH="/app/.venv/bin:${PATH}" + +WORKDIR /app + +# Sync dependencies first to maximize Docker layer caching. +COPY pyproject.toml ./ +RUN uv sync --no-install-project --no-cache + +# Now copy the rest of the agent and finalize the environment. +COPY . ./ +RUN uv sync --no-cache + +EXPOSE 8088 + +CMD ["uv", "run", "--no-sync", "python", "main.py"] + diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/README.md new file mode 100644 index 0000000000..e5b0ee90fc --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/README.md @@ -0,0 +1,116 @@ +# What this sample demonstrates + +An [Agent Framework](https://github.com/microsoft/agent-framework) agent with a +**Monty-backed CodeAct context provider** hosted using the **Responses protocol**. +The model receives one tool (`execute_code`) and runs Python inside a +[Monty](https://github.com/pydantic/monty) interpreter; the registered host +tools (`compute`, `fetch_data`) are only reachable from inside the sandbox via +typed `await compute(...)` calls or the generic `call_tool(...)` fallback. + +> [!NOTE] +> `agent-framework-monty` is an **alpha** package, so the `pyproject.toml` +> sets `[tool.uv] prerelease = "allow"` to let `uv sync` pick up the +> `1.0.0a*` release from PyPI. + +## How It Works + +### Model Integration + +The agent uses `FoundryChatClient` to create a Responses client from the project +endpoint and the model deployment. The agent supports both streaming (SSE +events) and non-streaming (JSON) response modes. + +See [main.py](main.py) for the full implementation. + +### CodeAct context provider + +`MontyCodeActProvider` is added to the agent via `context_providers=[...]`. On +every run it injects: + +- An `execute_code` tool that runs Python in the Monty interpreter. +- Dynamic CodeAct instructions describing the available host tools and DSL. + +The host tools (`compute`, `fetch_data`) are **not** exposed as direct agent +tools — the model can only call them from inside `execute_code`, either as +typed async functions (`await compute(operation="multiply", a=6, b=7)`) or via +the generic `call_tool("compute", operation="multiply", a=6, b=7)` fallback. +Code is type-checked against the host tool signatures using +[ty](https://docs.astral.sh/ty/) before any tool runs. + +OS-level access (filesystem, network, subprocess) is blocked inside the +sandbox; the registered host tools retain full Python access. + +### Observability + +Agent Framework's [native OpenTelemetry instrumentation](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) is enabled by setting these env vars in `agent.yaml` / `agent.manifest.yaml`: + +- `ENABLE_INSTRUMENTATION=true` — turns on the framework's span/metric/log emitters. +- `ENABLE_SENSITIVE_DATA=true` — includes prompts, tool inputs, tool outputs, and completions in telemetry. **Dev/test only.** + +`main.py` wires Azure Monitor at startup: + +1. Reads `APPLICATIONINSIGHTS_CONNECTION_STRING` (Foundry hosting injects this automatically for the project's attached Application Insights resource; set it yourself when running locally). +2. Calls `azure.monitor.opentelemetry.configure_azure_monitor(connection_string=...)` to register Azure Monitor exporters with the global OTel tracer/meter/logger providers. +3. Calls `agent_framework.observability.enable_instrumentation()` so Agent Framework emits its `invoke_agent`, `chat`, `execute_tool`, and `execute_code` spans on those providers. + +Trace linking happens automatically: the Foundry hosting layer's incoming `Responses` request becomes the **parent span**, and every framework / tool span (including the `execute_code` invocation that runs Monty) becomes a child via OpenTelemetry context propagation since both layers share the same global tracer provider. In Application Insights you can click any operation and see the full tree from inbound HTTP all the way down to individual `compute(...)` / `fetch_data(...)` calls inside the Monty sandbox. + +## Running the Agent Host + +This sample uses `pyproject.toml` + `uv sync` rather than the parent +README's `requirements.txt` flow. To run locally: + +1. Install dependencies into a local virtual environment: + + ```bash + uv sync + ``` + +2. Set the environment variables described in the + [parent README](../../README.md#running-the-agent-host-locally) (Foundry + project endpoint, model deployment, optional Application Insights), then + start the host: + + ```bash + uv run python main.py + ``` + +Refer to the parent README for the shared `azd` / Docker / invocation / +deployment guidance. + +## Interacting with the agent + +> Depending on how you run the agent host, you can invoke the agent using +> `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the +> [parent README](../../README.md) for more details. Use this README for +> sample queries you can send to the agent. + +Send a POST request to the server with a JSON body containing an `"input"` +field. Try queries that benefit from combining Python with multiple tool calls: + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Fetch all users, find the admins, then multiply the count by 7. Use a single execute_code call."}' +``` + +```bash +curl -X POST http://localhost:8088/responses \ + -H "Content-Type: application/json" \ + -d '{"input": "Compute the total price for one of every product in the products table. Use execute_code."}' +``` + +The model should respond with one `execute_code` call whose code looks like: + +```python +users = await fetch_data(table="users") +admins = [u for u in users if u["role"] == "admin"] +result = await compute(operation="multiply", a=len(admins), b=7) +print(result) +``` + +## Deploying the Agent to Foundry + +To host the agent on Foundry, follow the instructions in the +[Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) +section of the README in the parent directory. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.manifest.yaml new file mode 100644 index 0000000000..b406d26219 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.manifest.yaml @@ -0,0 +1,28 @@ +name: agent-framework-agent-monty-codeact-responses +description: > + An Agent Framework agent with a Monty-backed CodeAct context provider hosted by Foundry. +metadata: + tags: + - Agent Framework + - AI Agent Hosting + - Azure AI AgentServer + - Responses Protocol + - CodeAct + - Monty +template: + name: agent-framework-agent-monty-codeact-responses + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" + - name: ENABLE_INSTRUMENTATION + value: "true" + - name: ENABLE_SENSITIVE_DATA + value: "true" +resources: + - kind: model + id: gpt-4.1-mini + name: AZURE_AI_MODEL_DEPLOYMENT_NAME diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.yaml new file mode 100644 index 0000000000..8288362298 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/agent.yaml @@ -0,0 +1,15 @@ +kind: hosted +name: agent-framework-agent-monty-codeact-responses +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.25" + memory: 0.5Gi +environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: ENABLE_INSTRUMENTATION + value: "true" + - name: ENABLE_SENSITIVE_DATA + value: "true" diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/main.py new file mode 100644 index 0000000000..de7629f0f6 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/main.py @@ -0,0 +1,136 @@ +# Copyright (c) Microsoft. All rights reserved. + +import logging +import os +from typing import Annotated, Any, Literal + +from agent_framework import Agent, tool +from agent_framework.foundry import FoundryChatClient +from agent_framework.observability import enable_instrumentation +from agent_framework_foundry_hosting import ResponsesHostServer +from agent_framework_monty import MontyCodeActProvider +from azure.identity import DefaultAzureCredential +from dotenv import load_dotenv +from pydantic import Field + +# Load environment variables from .env file (no-op when injected by Foundry). +load_dotenv() + +logger = logging.getLogger(__name__) + + +def _setup_telemetry() -> None: + """Wire Agent Framework spans to the Application Insights resource attached to the Foundry project. + + Foundry-hosted runtimes inject ``APPLICATIONINSIGHTS_CONNECTION_STRING`` automatically; + locally you can set it yourself (see README). When the connection string is present we + configure Azure Monitor OTel exporters once and then flip the framework's instrumentation + flag so it emits ``invoke_agent`` / ``chat`` / ``execute_tool`` spans. The hosting layer's + incoming-request span becomes the parent automatically via OpenTelemetry context + propagation when both layers share the same global tracer provider. + """ + connection_string = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING") + if not connection_string: + logger.info( + "APPLICATIONINSIGHTS_CONNECTION_STRING is not set; Agent Framework spans will not " + "be exported to Azure Monitor. Set the env var to enable telemetry." + ) + return + + try: + from azure.monitor.opentelemetry import configure_azure_monitor + except ImportError: + logger.warning( + "azure-monitor-opentelemetry is not installed; skipping Azure Monitor setup. " + "Install it to export telemetry." + ) + return + + # Configure the global OTel providers (tracer/meter/logger) to export to Azure Monitor. + # Idempotent for repeated imports because we only call it from this entry point. + configure_azure_monitor(connection_string=connection_string) + # Flip the Agent Framework instrumentation flag so its spans are actually emitted on + # the now-configured global providers. + enable_instrumentation() + logger.info("Azure Monitor + Agent Framework instrumentation enabled.") + + +@tool(approval_mode="never_require") +def compute( + operation: Annotated[ + Literal["add", "subtract", "multiply", "divide"], + Field(description="Math operation: add, subtract, multiply, or divide."), + ], + a: Annotated[float, Field(description="First numeric operand.")], + b: Annotated[float, Field(description="Second numeric operand.")], +) -> float: + """Perform a math operation used by sandboxed code.""" + operations = { + "add": a + b, + "subtract": a - b, + "multiply": a * b, + "divide": a / b if b else float("inf"), + } + return operations[operation] + + +@tool(approval_mode="never_require") +def fetch_data( + table: Annotated[str, Field(description="Name of the simulated table to query.")], +) -> list[dict[str, Any]]: + """Fetch simulated records from a named table.""" + data: dict[str, list[dict[str, Any]]] = { + "users": [ + {"id": 1, "name": "Alice", "role": "admin"}, + {"id": 2, "name": "Bob", "role": "user"}, + {"id": 3, "name": "Charlie", "role": "admin"}, + ], + "products": [ + {"id": 101, "name": "Widget", "price": 9.99}, + {"id": 102, "name": "Gadget", "price": 19.99}, + ], + } + return data.get(table, []) + + +def main() -> None: + """Host a Monty CodeAct agent over the Responses protocol.""" + # Set up telemetry BEFORE building the client/agent so the framework picks up + # the configured tracer provider when it lazily wires instrumentation. + _setup_telemetry() + + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=DefaultAzureCredential(), + ) + + # MontyCodeActProvider injects a sandboxed `execute_code` tool into every + # agent run, plus dynamic instructions describing the registered host tools. + # The host tools are hidden from the model - they can only be invoked from + # inside the sandbox (`await compute(...)` or `call_tool(...)`). + codeact = MontyCodeActProvider( + tools=[compute, fetch_data], + approval_mode="never_require", + ) + + agent = Agent( + client=client, + instructions=( + "You are a friendly assistant. Use `execute_code` to combine " + "Python control flow with the provided host tools whenever the " + "task requires lookups, transformations, or computation." + ), + context_providers=[codeact], + # History will be managed by the hosting infrastructure, thus there + # is no need to store history by the service. Learn more at: + # https://developers.openai.com/api/reference/resources/responses/methods/create + default_options={"store": False}, + ) + + server = ResponsesHostServer(agent) + server.run() + + +if __name__ == "__main__": + main() diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/pyproject.toml b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/pyproject.toml new file mode 100644 index 0000000000..4abc446619 --- /dev/null +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/11_monty_codeact/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "agent-framework-agent-monty-codeact-responses" +version = "0.1.0" +description = "Foundry-hosted Agent Framework agent with a Monty-backed CodeAct context provider." +requires-python = ">=3.12,<3.14" +dependencies = [ + "agent-framework-foundry", + "agent-framework-foundry-hosting", + # agent-framework-monty is an alpha (1.0.0a*) release on PyPI. + "agent-framework-monty", + # Azure Monitor OpenTelemetry exporter; used to send agent telemetry to the + # Application Insights instance attached to the Foundry project. + "azure-monitor-opentelemetry", +] + +[tool.uv] +# `agent-framework-monty` is an alpha package; allow the prerelease resolver +# to pick up 1.0.0a* releases from PyPI. +prerelease = "allow" + diff --git a/python/uv.lock b/python/uv.lock index 9051f077fc..79b570bbf8 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -50,6 +50,7 @@ members = [ "agent-framework-hyperlight", "agent-framework-lab", "agent-framework-mem0", + "agent-framework-monty", "agent-framework-ollama", "agent-framework-openai", "agent-framework-orchestrations", @@ -720,6 +721,21 @@ requires-dist = [ { name = "mem0ai", specifier = ">=1.0.0,<2" }, ] +[[package]] +name = "agent-framework-monty" +version = "1.0.0a260518" +source = { editable = "packages/monty" } +dependencies = [ + { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic-monty", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework-core", editable = "packages/core" }, + { name = "pydantic-monty", specifier = ">=0,<0.1" }, +] + [[package]] name = "agent-framework-ollama" version = "1.0.0b260519" @@ -5737,6 +5753,77 @@ wheels = [ { 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]] +name = "pydantic-monty" +version = "0.0.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/f8/431ba0b79d02922811392c4e3d283d6508f7052ceaa1936cc34878703ecc/pydantic_monty-0.0.17.tar.gz", hash = "sha256:9c4904a8fbc63282793f3afd2d180124494c7fc371783f365e5691c9586360af", size = 1007724, upload-time = "2026-04-22T20:13:48.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/19/8105bc0b3acb42f6cb48a29669a5e21316bc05e3e9b6fab64cf94b483712/pydantic_monty-0.0.17-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3c3b6c026d8a0437eeb4d6b2d908be75e2715e0555b9a13f076b7e9ba9bbae19", size = 7344730, upload-time = "2026-04-22T20:13:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a2/7281cdb37481c4252292b63bebf737c87d0fd463f3174499608607de0907/pydantic_monty-0.0.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c80b4d34437abd209c042f81f8ecea81a097022fb9b01431ab859b877edfbc4d", size = 7334937, upload-time = "2026-04-22T20:15:06.923Z" }, + { url = "https://files.pythonhosted.org/packages/a5/68/0bf7c0c627a56d8653b42888a3c1fc33cd33d2532ec456d9358275d7c792/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:beecc1f7e5b10db40d7b2b24a68166a36514289a2402bfef370a7984e90a2ab8", size = 7864543, upload-time = "2026-04-22T20:14:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/09/9b/5a6f006541fd3bdc64b6dfbbaeabfb2244c89a22d7077a1fc92ec497c03e/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64ea7babdcc9fba93089fa52589b6d0549f755e37500f6cf4aeaeb8e56328a3e", size = 7138764, upload-time = "2026-04-22T20:15:30.516Z" }, + { url = "https://files.pythonhosted.org/packages/01/cc/59cca979bd427d166df8c827fba9e794c4a5c08943e225a22adf9854a78f/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a7fe77a191205becb622eaecb075e8bcbbbe4dac20a916d9c58ce6d59a22a8da", size = 7444006, upload-time = "2026-04-22T20:15:23.386Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c5/d027170fb33fcbc038febb76dfd2d9047f5194a250ea608e3ed8e5ec28d4/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cdbefc180cc83c8b8415aaf95b9099bb2cb15261f40ebe2c92f13e7d52439a4", size = 7967564, upload-time = "2026-04-22T20:14:57.315Z" }, + { url = "https://files.pythonhosted.org/packages/3e/01/ac0d4bc1ff00acfac14b7cb2ee322d08778c206cd57f43da8206a2f6ce78/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:575ce5aa31db18bbbf6275f00e9b0c005ca393bfb73a2f306a577ad490ec2d98", size = 8199021, upload-time = "2026-04-22T20:15:14.488Z" }, + { url = "https://files.pythonhosted.org/packages/51/85/8d0c6e5f127da9ebc0fcda6e411592d12b7606347d67aecd4363df5eed6b/pydantic_monty-0.0.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e252ec54fc4728406045f7be36ca45dbea8e6856df9c6154b1b9821b8952dfa2", size = 7769814, upload-time = "2026-04-22T20:14:55.197Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cc/cb4d1b14b039eab00b33a7274f15f81739c3f272e2dfbeb8fb13c6b0c85d/pydantic_monty-0.0.17-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fba71e5cb49f15a1446ecee142c8cc11f4bd6df4fcb4926465c83181474b2fd4", size = 7317432, upload-time = "2026-04-22T20:14:19.993Z" }, + { url = "https://files.pythonhosted.org/packages/c8/16/737c7a023abbcb21848eb4d58f7167d9f4f8cdc46858ce8ed835cc2c137c/pydantic_monty-0.0.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69136647abd56f804987834e37573adcc5c3b3d05013b8b3a2939f44b3bd5199", size = 7767816, upload-time = "2026-04-22T20:13:40.002Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9f/5302b784f882ae8a8396f29f8c5ab4c16524c173a3d777b94af33858fdf2/pydantic_monty-0.0.17-cp310-cp310-win32.whl", hash = "sha256:d5b3beb6169b59adea10fdefb1e54bfa9a66165404891dfb6fcf16f7749cda3b", size = 7230648, upload-time = "2026-04-22T20:14:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/1c/27/8c219f619dad466ec25db365acf88e2a50450dd862e0daff0eb281b6176b/pydantic_monty-0.0.17-cp310-cp310-win_amd64.whl", hash = "sha256:50ed9561b6dd1a1863d4cac81e4eaca64cb10ab541aaab92fcb5996739bb8e7f", size = 8075073, upload-time = "2026-04-22T20:14:17.073Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/ca8e42d9f3318f5c454cf8b168d814ec97c6f2afc38756d4b1b806184f6d/pydantic_monty-0.0.17-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:af890d691f6055491a4e643dd5bf09e07bd7a20ad70038531aada6415ab8794a", size = 7344138, upload-time = "2026-04-22T20:13:29.155Z" }, + { url = "https://files.pythonhosted.org/packages/56/c8/cfaf0a56087301d4e88f72cf54ea45a7eebc09c021c85b8864447f1e3755/pydantic_monty-0.0.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f38a69858dfdd2c9474156616d05e25a288e2080aee24152fa40c19ad425f0e", size = 7334903, upload-time = "2026-04-22T20:14:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/51/77/a751a6f73f854aa85fed94cfa5ecab21d7bf218c9fa03c96f9edf470cc4e/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bb88264e291cee56770a775f57125538c4713c6d362e89ee63bff506f650a0df", size = 7864258, upload-time = "2026-04-22T20:13:15.594Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/2eb51eb37e9f712cada64fa8d7df4b63b1f5fc635290147ab158ff0e1ef1/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54c317611454aba8be7ca96aeeea9429f4702a5c4ba89812bea82bed0d8e34fd", size = 7138153, upload-time = "2026-04-22T20:14:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/bb/15/835b10cdec3b96b089eef9899df6850b7f84a10225c491698b0ecf8e532a/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9563b5b4933f0f08c0e66ec66aaa4f43f2388bcc04b984e58aab2146dacd3829", size = 7443572, upload-time = "2026-04-22T20:13:17.951Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/aca140923fad8a2821a135cfeaa2fbb3321063bbadaa760424a016bb1ac6/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35f267a501bc1910178a1515fdd3dd927273fbb44e44b8718cb3b33aee79f41b", size = 7967178, upload-time = "2026-04-22T20:14:06.032Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/7c4ff1e3fe2e82a4745decfca67b54a7a61cd306875e32d8e41c5192c69e/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35b000c52755f25f322ea7c4d079f09aa60635ffe24a6463899e423066a41bf3", size = 8198241, upload-time = "2026-04-22T20:15:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/30/0b/702db7b753b96ebc6713e7cbdfaecdb471df3e3cb0f0f6e828620a743b78/pydantic_monty-0.0.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61b517776ad13aa4580b1dd89188b18296ceeaf88256423563bbc99e804fd83f", size = 7768859, upload-time = "2026-04-22T20:13:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/8d16e0cc0c36d1444f25d57da68dd22216bf0961c457a482429cec32141b/pydantic_monty-0.0.17-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5da5362ef25665a23a3b13024497719f65cafa61d696cac76429f84701bee2e2", size = 7316674, upload-time = "2026-04-22T20:14:52.579Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/d47ae703d402e45475333c4bf11b117c8068305f00c1363dbaea13d0fd09/pydantic_monty-0.0.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e655b6ddd552c02b751f1d57fc291fbd5654ff8b166a8bd634857879160d0b7", size = 7767515, upload-time = "2026-04-22T20:15:16.539Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/e17fb50d0df5cf9908f8fffa25c5909ed0eb92ca102ded06f7a6d6133e78/pydantic_monty-0.0.17-cp311-cp311-win32.whl", hash = "sha256:ea8b3ae8c42d572cefad841d3bda63cc458d9de2361cb9172914250e6dbe2c75", size = 7230347, upload-time = "2026-04-22T20:14:08.083Z" }, + { url = "https://files.pythonhosted.org/packages/5e/82/d3119f59652d04bcf69d671ddbd38464d5775fbc738a258d3c8f7800e29d/pydantic_monty-0.0.17-cp311-cp311-win_amd64.whl", hash = "sha256:3293c2f7524bfc7c3d8c794f1c1dc1eb4cf9c65a5e222061e2218ced85f3f6df", size = 8074183, upload-time = "2026-04-22T20:14:50.42Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/95827babdb35149f076c5d191b6b1e7a7c58f4bc72432f905e02e4e3231e/pydantic_monty-0.0.17-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27c2254fa7a7b05e969f79578889230d293c62e0b1ee28371ec4f3c54b14426a", size = 7342248, upload-time = "2026-04-22T20:15:18.775Z" }, + { url = "https://files.pythonhosted.org/packages/cb/67/ca9cfc07cd445d22def53e9db86912f9ae3e11ef772ce41c2ff41a47eac5/pydantic_monty-0.0.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:445cc471ce6f5a88ef06741b7ebc7002a2253d182f55a2f47094d4adedaaf497", size = 7311255, upload-time = "2026-04-22T20:15:27.913Z" }, + { url = "https://files.pythonhosted.org/packages/df/96/abc9c4972d91a9673435b84e12b99d038e42d1f99648fc9e5f242e09d00e/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39121038405911f59da7bf61164251f59bad3fb1b0cd28f43c42c3949eee2c8a", size = 7868109, upload-time = "2026-04-22T20:15:04.779Z" }, + { url = "https://files.pythonhosted.org/packages/75/82/9e4d55529bb99d882b9277a721762537a8bc1345ab1d052bb614a88bd15b/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dafc8ffe57c257002f623afdb7d0e41f73de850179ebd90b42611e4f2b6f9884", size = 7139709, upload-time = "2026-04-22T20:15:25.386Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/f1af6acefb7bb38d73934d6853998bbd327de7418b811372519080d9fd84/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea00838ef8f37dcd8085defcbdfe89fdd05297a6533f1d3f4cad857d13cedc7b", size = 7450444, upload-time = "2026-04-22T20:13:37.974Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/af92ef409e1c065345cf1451bbcf19e00f70a250b8372ec65143ca9a9238/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683d18089acf14d0de293245b9e37c7f0ec64e6d266f6773144211931aa3ec97", size = 7967525, upload-time = "2026-04-22T20:13:42.674Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1f/23ecd6e268ef24ce6b0fe4a1e76a314990d2e923ac5791e29d657418243d/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1829993dd50cf497cbed66ea9f6c8ff7d157d22592a05c7399f92fb8a549e3c", size = 8199124, upload-time = "2026-04-22T20:15:00.02Z" }, + { url = "https://files.pythonhosted.org/packages/42/2a/36b694ea0c7e202250a81a57faf00f218738da6c5d070c752f2d81cd34ce/pydantic_monty-0.0.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7869e3f41a54cc588096c52a8a4de25ecd81e75c867ea4164b14ea1ae1a57f", size = 7739623, upload-time = "2026-04-22T20:14:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/98/e5/090357d7bc0f0751d1afbb71330695fa26554699c88ba56ecaad91657088/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9c17663e2c6f07aec5bc54cd7e39a9e20f250a97a9081e1b2b932eb00d0afc5", size = 7317755, upload-time = "2026-04-22T20:14:48.367Z" }, + { url = "https://files.pythonhosted.org/packages/15/63/67200070cf33325ecfda81d4aee3bf312250ce80bd73058103e04e0f3587/pydantic_monty-0.0.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5d2cf98afe2fb124f6ade91d9663d54277478cad417164f83df1854a41ef450c", size = 7769158, upload-time = "2026-04-22T20:14:39.611Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/9ecfbc2f45406cfb247fafdea4f4a8412db3e559a22c4385eb15266ba2c1/pydantic_monty-0.0.17-cp312-cp312-win32.whl", hash = "sha256:b2185cc4effbbd6793eed4e0f0bcb6a3dbfbb3289ea4d47888708813f0a3dd47", size = 7227917, upload-time = "2026-04-22T20:14:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/d3/80/9be3bef8273817ccc17da25c3ce4ff5d5d45e5629c17eacc90cdef073821/pydantic_monty-0.0.17-cp312-cp312-win_amd64.whl", hash = "sha256:7833daed757ec9b09b627cc3577a4a76b114c5148f779531d7cfdb1095bcf0a9", size = 8043469, upload-time = "2026-04-22T20:13:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/b5/44/0e106b8b27eb93b66e4f3d279486464e05ba5ee31088848e58b5f506f879/pydantic_monty-0.0.17-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0cdac8c3c16477596bc96ee1cec4f2fbaccd089e2daa1e7b9f227cc89f97cb1", size = 7341507, upload-time = "2026-04-22T20:14:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/e5/88/a0315fa08e62e2d1ef00c03d8202d7bef3f1f71543bebfb916fea265c0a2/pydantic_monty-0.0.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37290d6a1c35aba5cfb8b490bb31c0d822e8ddca8f3ca9ea068e30930d80dd1e", size = 7311916, upload-time = "2026-04-22T20:13:34.022Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e5/e4da6acb408594cbfbfb8dd3c0491b9b2ee54e9183e7ebc5f584baa07af9/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:49f252b2fb918686d3e8f76cb30245e782d1560a7fa68dbc0f6940d83c12bd41", size = 7867465, upload-time = "2026-04-22T20:13:31.466Z" }, + { url = "https://files.pythonhosted.org/packages/58/d4/64c2f8eb708a743b0944ea8f71dfd51bc655285b4be28d55577dafbb29fa/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2254d25c34463d67069f5f1567157bde9311654cd5371f8933b8ea9815bfa26a", size = 7139262, upload-time = "2026-04-22T20:14:10.715Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/5d766f9cd304e871a5dfe5f0a85eaa533538ef07e9b2858fdf9f37f83694/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0688a1fa5dc045ac7b7e996d7269b94f74f116d7b1e352c7b5bb5ad53d4fe03", size = 7450119, upload-time = "2026-04-22T20:13:44.515Z" }, + { url = "https://files.pythonhosted.org/packages/33/98/fa16779021d93edb19807e87cdba56bbec6adfad21f10b41a212572ce513/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b35121ac555ed201405c69d531e4cb916da6984b8cd2e15c8a319117349faf6", size = 7967398, upload-time = "2026-04-22T20:13:55.576Z" }, + { url = "https://files.pythonhosted.org/packages/92/64/287a42720bc9e975ab5b52625aa9fc6bcef8298dd821022cc45c6ee1808d/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c97dc44af25d4392b474902fc40f78f09fe8f44ba791364670334fe12abc077", size = 8198835, upload-time = "2026-04-22T20:15:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/97/37/03edb1fd582b79b2b462afc3fea5e1c8fea73afefc4870dc35fc3c7c492e/pydantic_monty-0.0.17-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ed2fb365ef9ca921de9a17786ecfa2efe06e65678e6ca57be51658a2a880f31", size = 7739241, upload-time = "2026-04-22T20:13:46.925Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/b765c9ca2ae27def1caa07345aba073ae1239fc2d9cca7a375f3dc2195f7/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5bf9f07b38dd12747e3c95b169a5afb3e2e9107622e01e548246c84d19a69c99", size = 7316719, upload-time = "2026-04-22T20:14:13.058Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3b/64fe872cd575ab5262e1ba2959554ead198c939cfaa425f7f8e9b1ad2694/pydantic_monty-0.0.17-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e9bafd4b5acbc0a8e12ee8403a3ce37281c3b0fa5909d3f412bee76c69003c", size = 7769150, upload-time = "2026-04-22T20:13:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/76/b4/b6a0bb41f39bac2e11e6a2fd42ca0886893fafa6344fb17c3f0a94e22e83/pydantic_monty-0.0.17-cp313-cp313-win32.whl", hash = "sha256:1c239ae3e610d3f39cd1609285209a4e2d046b465ac1bfed0d4374c615eed0fa", size = 7227705, upload-time = "2026-04-22T20:15:12.262Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/d8cd62f537f7ab17714ee19ea221a0e341dab407215376ab1c41d79794c9/pydantic_monty-0.0.17-cp313-cp313-win_amd64.whl", hash = "sha256:1886c3590b02f359ae991f1e76691064f167330eda4fbf22762127ce17d0eb48", size = 8043469, upload-time = "2026-04-22T20:14:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/69/c0/8354baf835e1a04c4b9e11d253f82df7d625a9305e6a23a177fb895b1484/pydantic_monty-0.0.17-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a166bd04d1996f0d144fbb5e1391cd1c0fbdacd4fa3b689dd48931388679fe98", size = 7341303, upload-time = "2026-04-22T20:14:35.653Z" }, + { url = "https://files.pythonhosted.org/packages/00/ac/d58221b5e17915421ca00bb08b805ac121b6accb194785d5422da4a2f5fc/pydantic_monty-0.0.17-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d82f319e3fd79707a7b81bbb68596509d14ac73502d2f0daf4ab5d281efdfbdc", size = 7318912, upload-time = "2026-04-22T20:13:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/81/3f/8eeb8f652f6cd6e06a737aa9f00de2949e37a669b31756bc51b6182457b4/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f38b9875f7ff56fe69538b60b2ecbdcbe2b8b7780407ceb05fe0ee1414bf8d19", size = 7867027, upload-time = "2026-04-22T20:13:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/ec6620c27c8b4cada6ce53378c52c245e238e50a20b968cafdc1b8573c4e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74a778bb5a4dcdbc85b3b9002f9a72a43fb6ffd88635bfdd502e8d3053008337", size = 7137542, upload-time = "2026-04-22T20:13:35.939Z" }, + { url = "https://files.pythonhosted.org/packages/41/78/5419785630511b54b15cfb094871bcb53ec9025ba8d91bd7ab5b22b6c98f/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e2ab074a65738e9c1b4be9a432e7ca1e9987a6706018dc7a5af6d4ce7cecdf3", size = 7450222, upload-time = "2026-04-22T20:13:53.378Z" }, + { url = "https://files.pythonhosted.org/packages/61/04/cec11fa96a47034da3c21af53e73f43d2270f5ce96cd710809859fe9c0b2/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00fd1cd28b4200c9ccd02629868486b366af1bfe1f0584d4c9e513b7a941a868", size = 7967405, upload-time = "2026-04-22T20:14:03.826Z" }, + { url = "https://files.pythonhosted.org/packages/81/e3/f2be0fb975100b6936ca36a8410098f10fab3b26729c0b0d1de2fac59ff3/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ea6684555bfd00cbe9d2df3e73caada3d82200c168c63cc475197b55b88401", size = 8199028, upload-time = "2026-04-22T20:13:26.802Z" }, + { url = "https://files.pythonhosted.org/packages/2c/43/358bdaa9c50d21fe4a25a71d43ce9af2d6796616fa47aca84f807433564e/pydantic_monty-0.0.17-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f804a03a3bbd0cf0ade1d4ce11b50ca6858e9c4440b27c746faf1d3c0a272954", size = 7749903, upload-time = "2026-04-22T20:15:09.626Z" }, + { url = "https://files.pythonhosted.org/packages/9a/da/8bcd0a78abf13edceca36aaf5c180fad963e4c2e042fd7247b9e96048306/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:b5476e6c08b86b0bea554b97ce9b142aac1177447f2d3c5751864b27991fd1b1", size = 7312704, upload-time = "2026-04-22T20:14:33.668Z" }, + { url = "https://files.pythonhosted.org/packages/88/49/5de8bb7f8b82c3ebb8f2485e0b7a40055b072193039d95dcb5d35fcba72c/pydantic_monty-0.0.17-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b5fcdcca45439844bee268686f37226dbf5803ec7a5945f5536c41419f151dac", size = 7768902, upload-time = "2026-04-22T20:14:29.389Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/500a002f1a52f17c8b8a989875da3e1590c18ce95c89856aa967e2348a36/pydantic_monty-0.0.17-cp314-cp314-win32.whl", hash = "sha256:4dd3e6e80a415e7272f7a7583a4f8e045096653f6074e181117eb61fc8fe3b45", size = 7227049, upload-time = "2026-04-22T20:14:01.871Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/b8f552f2a863778f49ebb708f18aaf0bfb275480a48965786e06efacf1c4/pydantic_monty-0.0.17-cp314-cp314-win_amd64.whl", hash = "sha256:36a8090a628e8cf91df8f66c721a71050ac8f48473d4992b9afbd9585941a647", size = 8062999, upload-time = "2026-04-22T20:14:44.006Z" }, +] + [[package]] name = "pydantic-settings" version = "2.14.1" From f390595188c1e8536af33bd8d1dfd731d6046828 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Wed, 20 May 2026 10:01:44 +0900 Subject: [PATCH 11/22] Bump to 1.0.0rc2 for unique version (#5965) --- python/packages/ag-ui/pyproject.toml | 2 +- python/uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index c1785b93f1..b07ec37837 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-framework-ag-ui" -version = "1.0.0rc1" +version = "1.0.0rc2" description = "AG-UI protocol integration for Agent Framework" readme = "README.md" license-files = ["LICENSE"] diff --git a/python/uv.lock b/python/uv.lock index 79b570bbf8..1b932afa8c 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -179,7 +179,7 @@ requires-dist = [ [[package]] name = "agent-framework-ag-ui" -version = "1.0.0rc1" +version = "1.0.0rc2" source = { editable = "packages/ag-ui" } dependencies = [ { name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, From dd1e615dad51221f7a35f2c73b391664b463da41 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 20 May 2026 11:05:24 +0100 Subject: [PATCH 12/22] .NET: Add A2AAgentOptions and align A2AAgent constructors with ChatClientAgent pattern (#5954) * .NET: Add A2AAgentOptions and align A2AAgent constructors with ChatClientAgent pattern Adds a new A2AAgentOptions class (Id, Name, Description, Clone) and an options-based constructor on A2AAgent, mirroring ChatClientAgent/ChatClientAgentOptions. The existing parameter-based constructor is preserved for backward compatibility and now delegates to the options-based one. Extension methods are extended with options-based overloads: - A2AClientExtensions.AsAIAgent(IA2AClient, A2AAgentOptions, ...) - A2AAgentCardExtensions.AsAIAgent(AgentCard, A2AAgentOptions, ...) - A2ACardResolverExtensions.GetAIAgentAsync(A2ACardResolver, A2AAgentOptions, ...) For card-based creation, user-supplied options override values from the agent card; Name and Description fall back to card values when not set. Options are cloned when stored on the agent to prevent post-construction mutation, matching the ChatClientAgent pattern. Resolves #5870. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments - Add Throw.IfNull(client) in A2AClientExtensions.AsAIAgent - Add Throw.IfNull(card) in A2AAgentCardExtensions.AsAIAgent - Clarify httpClient docs in A2ACardResolverExtensions.GetAIAgentAsync: it applies to the created A2A client, not to card discovery - Rename test methods from GetAIAgent_* to AsAIAgent_* to match the API under test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Microsoft.Agents.AI.A2A/A2AAgent.cs | 38 ++++++--- .../A2AAgentOptions.cs | 40 ++++++++++ .../Extensions/A2AAgentCardExtensions.cs | 37 +++++++++ .../Extensions/A2ACardResolverExtensions.cs | 36 +++++++++ .../Extensions/A2AClientExtensions.cs | 25 +++++- .../A2AAgentTests.cs | 66 ++++++++++++++++ .../Extensions/A2AAgentCardExtensionsTests.cs | 77 ++++++++++++++++++- .../A2ACardResolverExtensionsTests.cs | 55 +++++++++++++ .../Extensions/A2AClientExtensionsTests.cs | 47 ++++++++++- 9 files changed, 406 insertions(+), 15 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI.A2A/A2AAgentOptions.cs diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs index 43e8a53791..9a42359ffc 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs @@ -28,9 +28,7 @@ public sealed class A2AAgent : AIAgent private static readonly AIAgentMetadata s_agentMetadata = new("a2a"); private readonly IA2AClient _a2aClient; - private readonly string? _id; - private readonly string? _name; - private readonly string? _description; + private readonly A2AAgentOptions _agentOptions; private readonly ILogger _logger; /// @@ -38,17 +36,37 @@ public sealed class A2AAgent : AIAgent /// /// The A2A client to use for interacting with A2A agents. /// The unique identifier for the agent. - /// The the name of the agent. + /// The name of the agent. /// The description of the agent. /// Optional logger factory to use for logging. public A2AAgent(IA2AClient a2aClient, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) + : this( + a2aClient, + new A2AAgentOptions + { + Id = id, + Name = name, + Description = description + }, + loggerFactory) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The A2A client to use for interacting with A2A agents. + /// + /// Configuration options that control the agent's identity, including its identifier, name, and description. + /// + /// Optional logger factory to use for logging. + public A2AAgent(IA2AClient a2aClient, A2AAgentOptions options, ILoggerFactory? loggerFactory = null) { _ = Throw.IfNull(a2aClient); + _ = Throw.IfNull(options); this._a2aClient = a2aClient; - this._id = id; - this._name = name; - this._description = description; + this._agentOptions = options.Clone(); this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); } @@ -216,13 +234,13 @@ public sealed class A2AAgent : AIAgent } /// - protected override string? IdCore => this._id; + protected override string? IdCore => this._agentOptions.Id; /// - public override string? Name => this._name; + public override string? Name => this._agentOptions.Name; /// - public override string? Description => this._description; + public override string? Description => this._agentOptions.Description; /// public override object? GetService(Type serviceType, object? serviceKey = null) diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgentOptions.cs new file mode 100644 index 0000000000..154ebf4397 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgentOptions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.AI.A2A; + +/// +/// Represents configuration options for an , including its identifier, name, and description. +/// +/// +/// This class is used to encapsulate information about an A2A agent, such as its unique +/// identifier, display name, and a descriptive summary. It provides an alternative to passing +/// these values as individual constructor parameters. +/// +public sealed class A2AAgentOptions +{ + /// + /// Gets or sets the agent id. + /// + public string? Id { get; set; } + + /// + /// Gets or sets the agent name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the agent description. + /// + public string? Description { get; set; } + + /// + /// Creates a new instance of with the same values as this instance. + /// + public A2AAgentOptions Clone() + => new() + { + Id = this.Id, + Name = this.Name, + Description = this.Description + }; +} diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs index 086505b2bc..9579a58643 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAgentCardExtensions.cs @@ -2,7 +2,9 @@ using System.Net.Http; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.A2A; using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; namespace A2A; @@ -36,4 +38,39 @@ public static class A2AAgentCardExtensions return a2aClient.AsAIAgent(name: card.Name, description: card.Description, loggerFactory: loggerFactory); } + + /// + /// Retrieves an instance of for an existing A2A agent. + /// + /// + /// This method can be used to access A2A agents that support the + /// Curated Registries (Catalog-Based Discovery) + /// discovery mechanism. When is provided, any non-null values override + /// the corresponding values from the . + /// + /// The to use for the agent creation. + /// + /// Configuration options that control the agent's identity. When provided, non-null values override the + /// corresponding values from the agent card. + /// + /// The to use for HTTP requests. + /// + /// Optional controlling protocol binding preference. + /// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback. + /// + /// The logger factory for enabling logging within the agent. + /// An instance backed by the A2A agent. + public static AIAgent AsAIAgent(this AgentCard card, A2AAgentOptions agentOptions, HttpClient? httpClient = null, A2AClientOptions? clientOptions = null, ILoggerFactory? loggerFactory = null) + { + _ = Throw.IfNull(card); + _ = Throw.IfNull(agentOptions); + + var a2aClient = A2AClientFactory.Create(card, httpClient, clientOptions); + + var mergedOptions = agentOptions.Clone(); + mergedOptions.Name ??= card.Name; + mergedOptions.Description ??= card.Description; + + return a2aClient.AsAIAgent(mergedOptions, loggerFactory); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs index 49b6de1102..4590be1e05 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2ACardResolverExtensions.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Agents.AI; using Microsoft.Agents.AI.A2A; using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; namespace A2A; @@ -48,4 +49,39 @@ public static class A2ACardResolverExtensions return agentCard.AsAIAgent(httpClient, options, loggerFactory); } + + /// + /// Retrieves an instance of for an existing A2A agent. + /// + /// + /// This method can be used to access A2A agents that support the + /// Well-Known URI + /// discovery mechanism. When is provided, any non-null values override + /// the corresponding values from the resolved . + /// + /// The to use for the agent creation. + /// + /// Configuration options that control the agent's identity. When provided, non-null values override the + /// corresponding values from the resolved agent card. + /// + /// + /// The to use for HTTP requests made by the created A2A client. + /// This is not used for fetching the agent card; the resolver uses its own configured client for that. + /// + /// + /// Optional controlling protocol binding preference. + /// When not provided, defaults to preferring HTTP+JSON first, with JSON-RPC as fallback. + /// + /// The logger factory for enabling logging within the agent. + /// The to monitor for cancellation requests. The default is . + /// An instance backed by the A2A agent. + public static async Task GetAIAgentAsync(this A2ACardResolver resolver, A2AAgentOptions agentOptions, HttpClient? httpClient = null, A2AClientOptions? clientOptions = null, ILoggerFactory? loggerFactory = null, CancellationToken cancellationToken = default) + { + _ = Throw.IfNull(agentOptions); + + // Obtain the agent card from the resolver. + var agentCard = await resolver.GetAgentCardAsync(cancellationToken).ConfigureAwait(false); + + return agentCard.AsAIAgent(agentOptions, httpClient, clientOptions, loggerFactory); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs index c7386309d3..150adcd6a7 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AClientExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Agents.AI; using Microsoft.Agents.AI.A2A; using Microsoft.Extensions.Logging; +using Microsoft.Shared.Diagnostics; namespace A2A; @@ -31,10 +32,32 @@ public static class A2AClientExtensions /// /// The to use for the agent. /// The unique identifier for the agent. - /// The the name of the agent. + /// The name of the agent. /// The description of the agent. /// Optional logger factory for enabling logging within the agent. /// An instance backed by the A2A agent. public static AIAgent AsAIAgent(this IA2AClient client, string? id = null, string? name = null, string? description = null, ILoggerFactory? loggerFactory = null) => new A2AAgent(client, id, name, description, loggerFactory); + + /// + /// Retrieves an instance of for an existing A2A agent. + /// + /// + /// This method can be used to access A2A agents that support the + /// Direct Configuration / Private Discovery + /// discovery mechanism. + /// + /// The to use for the agent. + /// + /// Configuration options that control the agent's identity, including its identifier, name, and description. + /// + /// Optional logger factory for enabling logging within the agent. + /// An instance backed by the A2A agent. + public static AIAgent AsAIAgent(this IA2AClient client, A2AAgentOptions options, ILoggerFactory? loggerFactory = null) + { + _ = Throw.IfNull(client); + _ = Throw.IfNull(options); + + return new A2AAgent(client, options, loggerFactory); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs index dfbe0c17de..14553abee9 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/A2AAgentTests.cs @@ -83,6 +83,72 @@ public sealed class A2AAgentTests : IDisposable Assert.Null(agent.Description); } + [Fact] + public void Constructor_WithOptions_InitializesPropertiesCorrectly() + { + // Arrange + var options = new A2AAgentOptions + { + Id = "options-id", + Name = "options-name", + Description = "options-description" + }; + + // Act + var agent = new A2AAgent(this._a2aClient, options); + + // Assert + Assert.Equal("options-id", agent.Id); + Assert.Equal("options-name", agent.Name); + Assert.Equal("options-description", agent.Description); + } + + [Fact] + public void Constructor_WithOptions_IsolatesAgentFromOptionsMutation() + { + // Arrange + var options = new A2AAgentOptions + { + Id = "original-id", + Name = "Original Name", + Description = "Original Description" + }; + var agent = new A2AAgent(this._a2aClient, options); + + // Act - mutate options after agent construction + options.Id = "mutated-id"; + options.Name = "Mutated Name"; + options.Description = "Mutated Description"; + + // Assert - agent should retain original values + Assert.Equal("original-id", agent.Id); + Assert.Equal("Original Name", agent.Name); + Assert.Equal("Original Description", agent.Description); + } + + [Fact] + public void Constructor_WithNullOptions_ThrowsArgumentNullException() => + // Act & Assert + Assert.Throws(() => new A2AAgent(this._a2aClient, options: null!)); + + [Fact] + public void Constructor_WithEmptyOptions_UsesBaseProperties() + { + // Act + var agent = new A2AAgent(this._a2aClient, new A2AAgentOptions()); + + // Assert + Assert.NotNull(agent.Id); + Assert.NotEmpty(agent.Id); + Assert.Null(agent.Name); + Assert.Null(agent.Description); + } + + [Fact] + public void Constructor_WithOptions_NullA2AClient_ThrowsArgumentNullException() => + // Act & Assert + Assert.Throws(() => new A2AAgent(null!, new A2AAgentOptions())); + [Fact] public async Task RunAsync_AllowsNonUserRoleMessagesAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs index f709e95ea7..c605691ce2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAgentCardExtensionsTests.cs @@ -31,7 +31,7 @@ public sealed class A2AAgentCardExtensionsTests } [Fact] - public void GetAIAgent_ReturnsAIAgent() + public void AsAIAgent_ReturnsAIAgent() { // Act var agent = this._agentCard.AsAIAgent(); @@ -165,6 +165,81 @@ public sealed class A2AAgentCardExtensionsTests Assert.ThrowsAny(() => card.AsAIAgent()); } + [Fact] + public void AsAIAgent_WithAgentOptions_OverridesCardValues() + { + // Arrange + var card = new AgentCard + { + Name = "Card Agent", + Description = "Card description", + SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }] + }; + + var agentOptions = new A2AAgentOptions + { + Id = "custom-id", + Name = "Custom Agent", + Description = "Custom description" + }; + + // Act + var agent = card.AsAIAgent(agentOptions); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("custom-id", agent.Id); + Assert.Equal("Custom Agent", agent.Name); + Assert.Equal("Custom description", agent.Description); + } + + [Fact] + public void AsAIAgent_WithAgentOptions_FallsBackToCardValues() + { + // Arrange + var card = new AgentCard + { + Name = "Card Agent", + Description = "Card description", + SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }] + }; + + var agentOptions = new A2AAgentOptions + { + Id = "custom-id" + }; + + // Act + var agent = card.AsAIAgent(agentOptions); + + // Assert + Assert.NotNull(agent); + Assert.Equal("custom-id", agent.Id); + Assert.Equal("Card Agent", agent.Name); + Assert.Equal("Card description", agent.Description); + } + + [Fact] + public void AsAIAgent_WithEmptyAgentOptions_UsesCardValues() + { + // Arrange + var card = new AgentCard + { + Name = "Card Agent", + Description = "Card description", + SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }] + }; + + // Act + var agent = card.AsAIAgent(new A2AAgentOptions()); + + // Assert + Assert.NotNull(agent); + Assert.Equal("Card Agent", agent.Name); + Assert.Equal("Card description", agent.Description); + } + internal sealed class HttpMessageHandlerStub : HttpMessageHandler { public Queue ResponsesToReturn { get; } = new(); diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs index 8a664b7fc9..bdeae993f3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2ACardResolverExtensionsTests.cs @@ -113,6 +113,61 @@ public sealed class A2ACardResolverExtensionsTests : IDisposable Assert.Equal(new Uri("http://jsonrpc/agent"), this._handler.CapturedUris[1]); } + [Fact] + public async Task GetAIAgentAsync_WithAgentOptions_OverridesCardValuesAsync() + { + // Arrange + this._handler.ResponsesToReturn.Enqueue(new AgentCard + { + Name = "Card Agent", + Description = "Card description", + SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }] + }); + + var agentOptions = new A2AAgentOptions + { + Id = "custom-id", + Name = "Custom Agent", + Description = "Custom description" + }; + + // Act + var agent = await this._resolver.GetAIAgentAsync(agentOptions, httpClient: this._httpClient); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("custom-id", agent.Id); + Assert.Equal("Custom Agent", agent.Name); + Assert.Equal("Custom description", agent.Description); + } + + [Fact] + public async Task GetAIAgentAsync_WithAgentOptions_FallsBackToCardValuesAsync() + { + // Arrange + this._handler.ResponsesToReturn.Enqueue(new AgentCard + { + Name = "Card Agent", + Description = "Card description", + SupportedInterfaces = [new AgentInterface { Url = "http://test-endpoint/agent" }] + }); + + var agentOptions = new A2AAgentOptions + { + Id = "custom-id" + }; + + // Act + var agent = await this._resolver.GetAIAgentAsync(agentOptions, httpClient: this._httpClient); + + // Assert + Assert.NotNull(agent); + Assert.Equal("custom-id", agent.Id); + Assert.Equal("Card Agent", agent.Name); + Assert.Equal("Card description", agent.Description); + } + public void Dispose() { this._handler.Dispose(); diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs index 80b5107bf1..b33df689e7 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AClientExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.Agents.AI.A2A.UnitTests; public sealed class A2AClientExtensionsTests { [Fact] - public void GetAIAgent_WithAllParameters_ReturnsA2AAgentWithSpecifiedProperties() + public void AsAIAgent_WithAllParameters_ReturnsA2AAgentWithSpecifiedProperties() { // Arrange var a2aClient = new A2AClient(new Uri("http://test-endpoint")); @@ -32,7 +32,7 @@ public sealed class A2AClientExtensionsTests } [Fact] - public void GetAIAgent_WithIA2AClient_ReturnsA2AAgentWithSpecifiedProperties() + public void AsAIAgent_WithIA2AClient_ReturnsA2AAgentWithSpecifiedProperties() { // Arrange - use IA2AClient reference type to verify the extension method works with the interface IA2AClient a2aClient = new A2AClient(new Uri("http://test-endpoint")); @@ -53,7 +53,7 @@ public sealed class A2AClientExtensionsTests } [Fact] - public void GetAIAgent_WithIA2AClient_ExposesClientViaGetService() + public void AsAIAgent_WithIA2AClient_ExposesClientViaGetService() { // Arrange IA2AClient a2aClient = new A2AClient(new Uri("http://test-endpoint")); @@ -66,4 +66,45 @@ public sealed class A2AClientExtensionsTests Assert.NotNull(service); Assert.Same(a2aClient, service); } + + [Fact] + public void AsAIAgent_WithOptions_ReturnsA2AAgentWithSpecifiedProperties() + { + // Arrange + var a2aClient = new A2AClient(new Uri("http://test-endpoint")); + var options = new A2AAgentOptions + { + Id = "options-agent-id", + Name = "Options Agent", + Description = "Agent created with options" + }; + + // Act + var agent = a2aClient.AsAIAgent(options); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.Equal("options-agent-id", agent.Id); + Assert.Equal("Options Agent", agent.Name); + Assert.Equal("Agent created with options", agent.Description); + } + + [Fact] + public void AsAIAgent_WithEmptyOptions_ReturnsA2AAgentWithDefaultProperties() + { + // Arrange + var a2aClient = new A2AClient(new Uri("http://test-endpoint")); + + // Act + var agent = a2aClient.AsAIAgent(new A2AAgentOptions()); + + // Assert + Assert.NotNull(agent); + Assert.IsType(agent); + Assert.NotNull(agent.Id); + Assert.NotEmpty(agent.Id); + Assert.Null(agent.Name); + Assert.Null(agent.Description); + } } From 0ba552b84c0c076f16ff13e267957908c3063738 Mon Sep 17 00:00:00 2001 From: Baidar Date: Wed, 20 May 2026 13:50:26 +0200 Subject: [PATCH 13/22] Python: Skip MCP prompt loading when unsupported (#5370) * Python: Skip MCP prompt loading when unsupported * Fix MCP pagination pyright checks * Simplify MCP support flag checks --- python/packages/core/agent_framework/_mcp.py | 174 ++++++++++++++++--- python/packages/core/tests/core/test_mcp.py | 152 +++++++++++++++- 2 files changed, 299 insertions(+), 27 deletions(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 35ccb1d58a..b2942de2a0 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -255,7 +255,7 @@ class MCPTool: self._exit_stack = AsyncExitStack() self._lifecycle_lock = asyncio.Lock() self._lifecycle_request_lock = asyncio.Lock() - self._lifecycle_queue: asyncio.Queue[tuple[str, bool, asyncio.Future[None]]] | None = None + self._lifecycle_queue: asyncio.Queue[tuple[str, bool, bool, asyncio.Future[None]]] | None = None self._lifecycle_owner_task: asyncio.Task[None] | None = None self.session = session self.request_timeout = request_timeout @@ -265,6 +265,11 @@ class MCPTool: self.is_connected: bool = False self._tools_loaded: bool = False self._prompts_loaded: bool = False + self._server_capabilities: types.ServerCapabilities | None = None + self._supports_tools: bool = True + self._supports_prompts: bool = True + self._supports_logging: bool | None = None + self._ping_available: bool = True self._pending_reload_tasks: set[asyncio.Task[None]] = set() def __str__(self) -> str: @@ -566,11 +571,11 @@ class MCPTool: stop_error: BaseException | None = None try: while True: - action, reset, future = await queue.get() + action, reset, load_configured, future = await queue.get() try: if action == "connect": - await self._connect_on_owner(reset=reset) + await self._connect_on_owner(reset=reset, load_configured=load_configured) elif action == "close": await self._close_on_owner() else: @@ -595,7 +600,7 @@ class MCPTool: finally: while True: try: - _, _, future = queue.get_nowait() + _, _, _, future = queue.get_nowait() except asyncio.QueueEmpty: break if not future.done(): @@ -608,12 +613,18 @@ class MCPTool: owner_task = self._lifecycle_owner_task return owner_task is not None and asyncio.current_task() is owner_task - async def _run_on_lifecycle_owner(self, action: str, *, reset: bool = False) -> None: + async def _run_on_lifecycle_owner( + self, + action: str, + *, + reset: bool = False, + load_configured: bool = True, + ) -> None: await self._ensure_lifecycle_owner() if self._is_lifecycle_owner_task(): if action == "connect": - await self._connect_on_owner(reset=reset) + await self._connect_on_owner(reset=reset, load_configured=load_configured) elif action == "close": await self._close_on_owner() else: @@ -625,7 +636,7 @@ class MCPTool: raise RuntimeError("MCP lifecycle owner is not available.") future = asyncio.get_running_loop().create_future() - await queue.put((action, reset, future)) + await queue.put((action, reset, load_configured, future)) await future async def _safe_close_exit_stack(self) -> None: @@ -656,6 +667,32 @@ class MCPTool: await self._safe_close_exit_stack() return _should_propagate_cancelled_error(ex) + def _reset_session_state(self) -> None: + self._server_capabilities = None + self._supports_tools = True + self._supports_prompts = True + self._supports_logging = None + self._ping_available = True + + def _set_server_capabilities(self, capabilities: types.ServerCapabilities | None) -> None: + self._server_capabilities = capabilities + if capabilities is None: + self._supports_tools = False + self._supports_prompts = False + self._supports_logging = False + return + + self._supports_tools = getattr(capabilities, "tools", None) is not None + self._supports_prompts = getattr(capabilities, "prompts", None) is not None + self._supports_logging = getattr(capabilities, "logging", None) is not None + + async def _reconnect_without_loading(self) -> None: + if self._is_lifecycle_owner_task(): + await self._connect_on_owner(reset=True, load_configured=False) + return + + await self._run_on_lifecycle_owner("connect", reset=True, load_configured=False) + async def connect(self, *, reset: bool = False) -> None: if self._is_lifecycle_owner_task(): await self._connect_on_owner(reset=reset) @@ -664,7 +701,7 @@ class MCPTool: async with self._lifecycle_request_lock: await self._run_on_lifecycle_owner("connect", reset=reset) - async def _connect_on_owner(self, *, reset: bool = False) -> None: + async def _connect_on_owner(self, *, reset: bool = False, load_configured: bool = True) -> None: """Connect to the MCP server. Establishes a connection to the MCP server, initializes the session, @@ -672,6 +709,7 @@ class MCPTool: Keyword Args: reset: If True, forces a reconnection even if already connected. + load_configured: If True, loads tools and prompts according to the constructor flags. Raises: ToolException: If connection or session initialization fails. @@ -680,6 +718,7 @@ class MCPTool: await self._safe_close_exit_stack() self.session = None self.is_connected = False + self._reset_session_state() self._exit_stack = AsyncExitStack() if not self.session: try: @@ -741,7 +780,8 @@ class MCPTool: inner_exception=ex if isinstance(ex, Exception) else None, ) from ex try: - await session.initialize() + initialize_result = await session.initialize() + self._set_server_capabilities(getattr(initialize_result, "capabilities", None)) except (Exception, asyncio.CancelledError) as ex: if await self._close_and_check_cancelled(ex): raise @@ -759,17 +799,22 @@ class MCPTool: self.session = session elif self.session._request_id == 0: # type: ignore[attr-defined] # If the session is not initialized, we need to reinitialize it - await self.session.initialize() + initialize_result = await self.session.initialize() + self._set_server_capabilities(getattr(initialize_result, "capabilities", None)) + elif self._server_capabilities is None: + self._set_server_capabilities(getattr(self.session, "_server_capabilities", None)) logger.debug("Connected to MCP server: %s", self.session) self.is_connected = True - if self.load_tools_flag: - await self.load_tools() + if load_configured and self.load_tools_flag: + if self._supports_tools: + await self.load_tools() self._tools_loaded = True - if self.load_prompts_flag: - await self.load_prompts() + if load_configured and self.load_prompts_flag: + if self._supports_prompts: + await self.load_prompts() self._prompts_loaded = True - if logger.level != logging.NOTSET: + if logger.level != logging.NOTSET and self._supports_logging is not False: try: level_name = cast( Any, next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level) @@ -973,17 +1018,49 @@ class MCPTool: Raises: ToolExecutionException: If the MCP server is not connected. """ + from anyio import ClosedResourceError from mcp import types + if not self._supports_prompts: + logger.debug("Skipping MCP prompt loading because the server did not advertise prompts support.") + return + # Track existing function names to prevent duplicates existing_names = {func.name for func in self._functions} params: types.PaginatedRequestParams | None = None while True: - # Ensure connection is still valid before each page request - await self._ensure_connected() + prompt_list: types.ListPromptsResult | None = None + for attempt in range(2): + try: + # Ensure connection is still valid before each page request + await self._ensure_connected() + if not self._supports_prompts: + logger.debug( + "Skipping MCP prompt loading because the server did not advertise prompts support." + ) + return + prompt_list = await self.session.list_prompts(params=params) # type: ignore[union-attr] + break + except ClosedResourceError as cl_ex: + if attempt == 0: + logger.info("MCP connection closed unexpectedly while loading prompts. Reconnecting...") + try: + await self._reconnect_without_loading() + except Exception as reconn_ex: + raise ToolExecutionException( + "Failed to reconnect to MCP server.", + inner_exception=reconn_ex, + ) from reconn_ex + continue + logger.error("MCP connection closed unexpectedly after reconnection: %s", cl_ex) + raise ToolExecutionException( + "Failed to load prompts - connection lost.", + inner_exception=cl_ex, + ) from cl_ex - prompt_list = await self.session.list_prompts(params=params) # type: ignore[union-attr] + if prompt_list is None: + raise ToolExecutionException("Failed to load prompts.") for prompt in prompt_list.prompts: normalized_name = _normalize_mcp_name(prompt.name) @@ -1010,7 +1087,7 @@ class MCPTool: existing_names.add(local_name) # Check if there are more pages - if not prompt_list or not prompt_list.nextCursor: + if not prompt_list.nextCursor: break params = types.PaginatedRequestParams(cursor=prompt_list.nextCursor) @@ -1023,18 +1100,48 @@ class MCPTool: Raises: ToolExecutionException: If the MCP server is not connected. """ + from anyio import ClosedResourceError from mcp import types + if not self._supports_tools: + logger.debug("Skipping MCP tool loading because the server did not advertise tools support.") + return + # Track existing function names to prevent duplicates existing_names = {func.name for func in self._functions} self._tool_call_meta_by_name.clear() params: types.PaginatedRequestParams | None = None while True: - # Ensure connection is still valid before each page request - await self._ensure_connected() + tool_list: types.ListToolsResult | None = None + for attempt in range(2): + try: + # Ensure connection is still valid before each page request + await self._ensure_connected() + if not self._supports_tools: + logger.debug("Skipping MCP tool loading because the server did not advertise tools support.") + return + tool_list = await self.session.list_tools(params=params) # type: ignore[union-attr] + break + except ClosedResourceError as cl_ex: + if attempt == 0: + logger.info("MCP connection closed unexpectedly while loading tools. Reconnecting...") + try: + await self._reconnect_without_loading() + except Exception as reconn_ex: + raise ToolExecutionException( + "Failed to reconnect to MCP server.", + inner_exception=reconn_ex, + ) from reconn_ex + continue + logger.error("MCP connection closed unexpectedly after reconnection: %s", cl_ex) + raise ToolExecutionException( + "Failed to load tools - connection lost.", + inner_exception=cl_ex, + ) from cl_ex - tool_list = await self.session.list_tools(params=params) # type: ignore[union-attr] + if tool_list is None: + raise ToolExecutionException("Failed to load tools.") for tool in tool_list.tools: if tool.meta is not None: @@ -1083,7 +1190,7 @@ class MCPTool: existing_names.add(local_name) # Check if there are more pages - if not tool_list or not tool_list.nextCursor: + if not tool_list.nextCursor: break params = types.PaginatedRequestParams(cursor=tool_list.nextCursor) @@ -1100,6 +1207,7 @@ class MCPTool: self._exit_stack = AsyncExitStack() self.session = None self.is_connected = False + self._reset_session_state() async def close(self) -> None: """Disconnect from the MCP server. @@ -1131,12 +1239,30 @@ class MCPTool: Raises: ToolExecutionException: If reconnection fails. """ + from mcp.shared.exceptions import McpError + + if not self._ping_available: + return + try: await self.session.send_ping() # type: ignore[union-attr] + except McpError as mcp_exc: + if mcp_exc.error.code == -32601: + self._ping_available = False + logger.debug("Skipping future MCP pings because the server does not support ping.") + return + logger.info("MCP connection invalid or closed. Reconnecting...") + try: + await self._reconnect_without_loading() + except Exception as ex: + raise ToolExecutionException( + "Failed to establish MCP connection.", + inner_exception=ex, + ) from ex except Exception: logger.info("MCP connection invalid or closed. Reconnecting...") try: - await self.connect(reset=True) + await self._reconnect_without_loading() except Exception as ex: raise ToolExecutionException( "Failed to establish MCP connection.", diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index aea479ff86..6273eb76e6 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -4031,14 +4031,102 @@ async def test_connect_reinitializes_existing_session_and_loads_tools_and_prompt assert tool._prompts_loaded is True +async def test_connect_skips_tools_and_prompts_when_server_does_not_advertise_capabilities() -> None: + tool = MCPTool(name="test_tool", load_tools=True, load_prompts=True) + tool.is_connected = True + tool.session = Mock() + tool.session._request_id = 0 + tool.session.initialize = AsyncMock( + return_value=types.InitializeResult( + protocolVersion=types.LATEST_PROTOCOL_VERSION, + capabilities=types.ServerCapabilities(), + serverInfo=types.Implementation(name="test", version="1.0"), + ) + ) + tool.session.list_tools = AsyncMock() + tool.session.list_prompts = AsyncMock() + tool.session.set_logging_level = AsyncMock() + + with patch.object(logger, "level", logging.INFO): + await tool._connect_on_owner() + + tool.session.initialize.assert_awaited_once() + tool.session.list_tools.assert_not_called() + tool.session.list_prompts.assert_not_called() + tool.session.set_logging_level.assert_not_called() + assert tool.is_connected is True + assert tool._supports_tools is False + assert tool._supports_prompts is False + assert tool._supports_logging is False + assert tool._tools_loaded is True + assert tool._prompts_loaded is True + + +async def test_connect_treats_missing_capabilities_as_unsupported() -> None: + tool = MCPTool(name="test_tool", load_tools=True, load_prompts=True) + tool.is_connected = True + tool.session = Mock() + tool.session._request_id = 0 + tool.session.initialize = AsyncMock(return_value=Mock(capabilities=None)) + tool.session.list_tools = AsyncMock() + tool.session.list_prompts = AsyncMock() + + with patch.object(logger, "level", logging.NOTSET): + await tool._connect_on_owner() + + tool.session.list_tools.assert_not_called() + tool.session.list_prompts.assert_not_called() + assert tool._supports_tools is False + assert tool._supports_prompts is False + assert tool._supports_logging is False + + +async def test_connect_sets_logging_level_when_server_advertises_logging() -> None: + tool = MCPTool(name="test_tool", load_tools=False, load_prompts=False) + tool.is_connected = True + tool.session = Mock() + tool.session._request_id = 0 + tool.session.initialize = AsyncMock( + return_value=types.InitializeResult( + protocolVersion=types.LATEST_PROTOCOL_VERSION, + capabilities=types.ServerCapabilities(logging=types.LoggingCapability()), + serverInfo=types.Implementation(name="test", version="1.0"), + ) + ) + tool.session.set_logging_level = AsyncMock() + + with patch.object(logger, "level", logging.INFO): + await tool._connect_on_owner() + + tool.session.set_logging_level.assert_awaited_once_with("info") + assert tool._supports_logging is True + + +async def test_ensure_connected_skips_future_pings_when_ping_is_not_available() -> None: + tool = MCPTool(name="test_tool") + tool.session = Mock( + send_ping=AsyncMock( + side_effect=McpError(types.ErrorData(code=-32601, message="Method 'ping' is not available.")) + ) + ) + + with patch.object(tool, "_reconnect_without_loading", AsyncMock()) as mock_reconnect: + await tool._ensure_connected() + await tool._ensure_connected() + + tool.session.send_ping.assert_awaited_once() + mock_reconnect.assert_not_awaited() + assert tool._ping_available is False + + async def test_ensure_connected_reconnects_on_failed_ping() -> None: tool = MCPTool(name="test_tool") tool.session = Mock(send_ping=AsyncMock(side_effect=RuntimeError("closed"))) - with patch.object(tool, "connect", AsyncMock()) as mock_connect: + with patch.object(tool, "_reconnect_without_loading", AsyncMock()) as mock_reconnect: await tool._ensure_connected() - mock_connect.assert_awaited_once_with(reset=True) + mock_reconnect.assert_awaited_once_with() async def test_ensure_connected_wraps_reconnect_failure() -> None: @@ -4046,12 +4134,70 @@ async def test_ensure_connected_wraps_reconnect_failure() -> None: tool.session = Mock(send_ping=AsyncMock(side_effect=RuntimeError("closed"))) with ( - patch.object(tool, "connect", AsyncMock(side_effect=RuntimeError("still closed"))), + patch.object(tool, "_reconnect_without_loading", AsyncMock(side_effect=RuntimeError("still closed"))), pytest.raises(ToolExecutionException, match="Failed to establish MCP connection"), ): await tool._ensure_connected() +async def test_load_tools_reconnects_on_closed_resource_when_ping_is_unavailable() -> None: + from anyio import ClosedResourceError + + tool = MCPTool(name="test_tool", load_tools=True) + tool._ping_available = False + + first_session = Mock() + first_session.list_tools = AsyncMock(side_effect=ClosedResourceError()) + tool.session = first_session + + page = Mock() + page.tools = [] + page.nextCursor = None + + second_session = Mock() + second_session.list_tools = AsyncMock(return_value=page) + + async def reconnect() -> None: + tool.session = second_session + tool._supports_tools = True + + with patch.object(tool, "_reconnect_without_loading", AsyncMock(side_effect=reconnect)) as mock_reconnect: + await tool.load_tools() + + first_session.list_tools.assert_awaited_once() + mock_reconnect.assert_awaited_once_with() + second_session.list_tools.assert_awaited_once() + + +async def test_load_prompts_reconnects_on_closed_resource_when_ping_is_unavailable() -> None: + from anyio import ClosedResourceError + + tool = MCPTool(name="test_tool", load_prompts=True) + tool._ping_available = False + + first_session = Mock() + first_session.list_prompts = AsyncMock(side_effect=ClosedResourceError()) + tool.session = first_session + + page = Mock() + page.prompts = [] + page.nextCursor = None + + second_session = Mock() + second_session.list_prompts = AsyncMock(return_value=page) + + async def reconnect() -> None: + tool.session = second_session + tool._supports_prompts = True + + with patch.object(tool, "_reconnect_without_loading", AsyncMock(side_effect=reconnect)) as mock_reconnect: + await tool.load_prompts() + + first_session.list_prompts.assert_awaited_once() + mock_reconnect.assert_awaited_once_with() + second_session.list_prompts.assert_awaited_once() + + async def test_mcp_tool_filters_framework_kwargs(): """Test that call_tool filters out framework-specific kwargs before calling MCP session. From 72a6157c6aa3f8b49cbd257ef89059c439781994 Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Wed, 20 May 2026 04:52:08 -0700 Subject: [PATCH 14/22] [BREAKING] Python: Enable instrumentation by default (#5865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enable instrumentation by default * Update samples * Optimization when span is not recording * Address Copilot comments * Revert uv.lock * Add warning * Formatting * Fix mypy * Add disable_instrumentation() with sticky user-intent semantics Add a public disable_instrumentation() entry point so users can explicitly opt out of Agent Framework telemetry, with a sticky-disable flag that makes the user's intent "leading" — no framework code path (foundry's configure_azure_monitor, configure_otel_providers, enable_instrumentation, enable_sensitive_telemetry, or direct OBSERVABILITY_SETTINGS.enable_* writes) can re-enable instrumentation until the user explicitly clears the disable with enable_instrumentation(force=True) / enable_sensitive_telemetry(force=True). Also addresses the two remaining unresolved review threads on the PR: 1. test_observability_settings_defaults_instrumentation_true pins the new "ENABLE_INSTRUMENTATION defaults to True when env unset" behavior. 2. test_enable_instrumentation_reads_env_sensitive_data restores coverage for the post-import load_dotenv() fallback path. Implementation: - ObservabilitySettings.enable_instrumentation / enable_sensitive_data become properties backed by _enable_*. While _user_disabled is True, the getters return False and the setters drop True writes (defense in depth so third- party writes can't subvert the disable). - Public is_user_disabled read-only property lets integrations (e.g. foundry's configure_azure_monitor) cheaply check the disable state without poking at privates. - enable_instrumentation() and enable_sensitive_telemetry() short-circuit with an info log when disabled; gain a force=True kwarg that clears the disable. - configure_otel_providers() still creates providers / exporters / views so a later force-enable can use them, but logs an info message when called while disabled. - Foundry's FoundryChatClient.configure_azure_monitor and FoundryAgent.configure_azure_monitor early-return when the user has disabled, so Azure Monitor's global providers aren't installed unnecessarily. Tests: 11 new tests covering default-on, env re-read at call time, sticky behavior against each re-enable surface (enable_instrumentation, enable_sensitive_telemetry, configure_otel_providers, direct attribute writes), force=True override, re-arming the disable, and the __all__ export. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: document disable_instrumentation() and force=True paths Add a "Disabling instrumentation" section to the observability sample README that walks through: - The distinction between the ENABLE_INSTRUMENTATION env var (initial, non-sticky) and disable_instrumentation() (process-wide, sticky). - Why the sticky semantics matter: framework integrations like FoundryChatClient.configure_azure_monitor() can call enable_instrumentation() as part of their setup, and the user's opt-out needs to win. - All five surfaces guarded by the sticky disable (property reads, public enable functions, configure_otel_providers, direct attribute writes, is_user_disabled-aware integrations). - The force=True escape hatch on both enable_instrumentation() and enable_sensitive_telemetry(). - How third-party integrations should consult OBSERVABILITY_SETTINGS.is_user_disabled. - The limits of the disable (does not tear down existing providers / in-flight spans / third-party instrumentation, does not persist across processes). Cross-links the new section from the ENABLE_INSTRUMENTATION row in the env vars table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: soften disable_instrumentation() overclaim about telemetry guarantees Replace 'no telemetry will be emitted no matter what' (which is too strong, since callers can still pass force=True or mutate private attributes) with language framing the disable as a user-intent contract that library and framework code is expected to honor: the framework actively short-circuits the public enable paths, force=True and private-attribute writes are acknowledged as out-of-contract escape hatches that integrations should not use on the user's behalf. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: correct observability Dependencies section - opentelemetry-sdk is no longer a hard dependency; it is lazily imported by create_resource(), create_metric_views(), and configure_otel_providers() with a clear ImportError when missing. Day-to-day instrumentation works with opentelemetry-api alone provided some other component configures the global OpenTelemetry providers (Azure Monitor, an APM agent, application bootstrap, etc.). - opentelemetry-semantic-conventions-ai is no longer used anywhere in the source; remove it from the listed dependencies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: replace stale observability migration guide with current PR's only relevant migration The old guide documented the move away from setup_observability(otlp_endpoint=...) which was an earlier-release API change unrelated to this PR and stale enough that it's more confusing than helpful at this point. Replace it with a short note on the single migration this PR introduces: callers of enable_instrumentation(enable_sensitive_data=True) should switch to enable_sensitive_telemetry(). Cross-link to the Disabling instrumentation section for the rare 'force on without enabling sensitive data' use case where enable_instrumentation() still applies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Eduard van Valkenburg Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/.env.example | 3 +- .../skills/python-development/SKILL.md | 2 +- python/CODING_STANDARD.md | 2 +- .../agent_framework_chatkit/_converter.py | 11 +- .../core/agent_framework/observability.py | 196 +++++- .../core/tests/core/test_observability.py | 597 ++++++++++++++---- .../foundry/agent_framework_foundry/_agent.py | 16 +- .../agent_framework_foundry/_chat_client.py | 16 +- .../02-agents/observability/.env.example | 6 +- .../samples/02-agents/observability/README.md | 362 ++++++----- .../02-agents/observability/__init__.py | 0 .../advanced_manual_setup_console_output.py | 5 +- .../observability/advanced_zero_code.py | 17 +- .../observability/executor_io_observation.py | 3 - .../responses/07_observability/.env.example | 1 - .../responses/07_observability/README.md | 2 +- .../07_observability/agent.manifest.yaml | 2 - .../responses/07_observability/agent.yaml | 8 +- python/samples/README.md | 32 +- python/uv.lock | 2 +- 20 files changed, 923 insertions(+), 360 deletions(-) delete mode 100644 python/samples/02-agents/observability/__init__.py diff --git a/python/.env.example b/python/.env.example index bff78961aa..eab84910b1 100644 --- a/python/.env.example +++ b/python/.env.example @@ -44,7 +44,6 @@ GEMINI_MODEL="" # Ollama OLLAMA_ENDPOINT="" OLLAMA_MODEL="" -# Observability -ENABLE_INSTRUMENTATION=true +# Observability (instrumentation is enabled by default; set "ENABLE_INSTRUMENTATION" to "false" to opt out) ENABLE_SENSITIVE_DATA=true OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317/" diff --git a/python/.github/skills/python-development/SKILL.md b/python/.github/skills/python-development/SKILL.md index d3bb38ca4b..4214c08bdb 100644 --- a/python/.github/skills/python-development/SKILL.md +++ b/python/.github/skills/python-development/SKILL.md @@ -72,7 +72,7 @@ def equal(arg1: str, arg2: str) -> bool: from agent_framework import Agent, Message, tool # Components -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry # Connectors (lazy-loaded) from agent_framework.openai import OpenAIChatClient diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index 8c73414f3f..a9140ca353 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -186,7 +186,7 @@ The package follows a flat import structure: - **Components**: Import from `agent_framework.` ```python - from agent_framework.observability import enable_instrumentation, configure_otel_providers + from agent_framework.observability import enable_sensitive_telemetry, configure_otel_providers ``` - **Connectors**: Import from `agent_framework.` diff --git a/python/packages/chatkit/agent_framework_chatkit/_converter.py b/python/packages/chatkit/agent_framework_chatkit/_converter.py index f108c28312..dc322377d3 100644 --- a/python/packages/chatkit/agent_framework_chatkit/_converter.py +++ b/python/packages/chatkit/agent_framework_chatkit/_converter.py @@ -5,7 +5,6 @@ from __future__ import annotations import logging -import sys from collections.abc import Awaitable, Callable, Sequence from agent_framework import ( @@ -31,11 +30,6 @@ from chatkit.types import ( WorkflowItem, ) -if sys.version_info >= (3, 11): - from typing import assert_never # type:ignore # pragma: no cover -else: - from typing_extensions import assert_never # type:ignore # pragma: no cover - logger = logging.getLogger(__name__) @@ -532,7 +526,10 @@ class ThreadItemConverter: # TODO(evmattso): Implement structured input handling in a future PR return [] case _: - assert_never(item) + # Unknown ThreadItem variant (e.g. types added in newer chatkit versions). + # Skip rather than fail so we remain forward-compatible with chatkit upgrades. + logger.debug("Skipping unsupported ThreadItem of type %s", type(item).__name__) + return [] async def to_agent_input( self, diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index d324caa757..022008c05b 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -4,6 +4,8 @@ Commonly used exports: - enable_instrumentation +- disable_instrumentation +- enable_sensitive_telemetry - configure_otel_providers - AgentTelemetryLayer - ChatTelemetryLayer @@ -80,7 +82,9 @@ __all__ = [ "configure_otel_providers", "create_metric_views", "create_resource", + "disable_instrumentation", "enable_instrumentation", + "enable_sensitive_telemetry", "get_meter", "get_tracer", ] @@ -643,8 +647,8 @@ class ObservabilitySettings: Sensitive events should only be enabled on test and development environments. Keyword Args: - enable_instrumentation: Enable OpenTelemetry diagnostics. Default is False. - Can be set via environment variable ENABLE_INSTRUMENTATION. + enable_instrumentation: Enable OpenTelemetry diagnostics. Default is True. + Can be disabled by setting environment variable ENABLE_INSTRUMENTATION=false. enable_sensitive_data: Enable OpenTelemetry sensitive events. Default is False. Can be set via environment variable ENABLE_SENSITIVE_DATA. enable_console_exporters: Enable console exporters for traces, logs, and metrics. @@ -659,12 +663,12 @@ class ObservabilitySettings: from agent_framework import ObservabilitySettings # Using environment variables - # Set ENABLE_INSTRUMENTATION=true + # Instrumentation is enabled by default; set ENABLE_INSTRUMENTATION=false to disable. # Set ENABLE_CONSOLE_EXPORTERS=true settings = ObservabilitySettings() # Or passing parameters directly - settings = ObservabilitySettings(enable_instrumentation=True, enable_console_exporters=True) + settings = ObservabilitySettings(enable_console_exporters=True) """ def __init__(self, **kwargs: Any) -> None: @@ -677,14 +681,74 @@ class ObservabilitySettings: env_file_encoding=env_file_encoding, **kwargs, ) - self.enable_instrumentation: bool = data.get("enable_instrumentation") or False - self.enable_sensitive_data: bool = data.get("enable_sensitive_data") or False + # Sticky-disable flag, set by `disable_instrumentation()`. When True, this + # singleton refuses to be re-enabled by any subsequent assignment to the + # `enable_instrumentation` / `enable_sensitive_data` properties (including + # direct third-party writes). It can only be cleared by an explicit + # `enable_instrumentation(force=True)` / `enable_sensitive_telemetry(force=True)` + # call, which is the user re-stating their intent. + self._user_disabled: bool = False + # `enable_instrumentation` is defaulted to True if not set + instrumentation_value = data.get("enable_instrumentation") + self._enable_instrumentation: bool = True if instrumentation_value is None else instrumentation_value + self._enable_sensitive_data: bool = data.get("enable_sensitive_data") or False + if self._enable_sensitive_data and not self._enable_instrumentation: + logger.warning( + "Sensitive data capture is enabled but instrumentation is disabled. " + "Sensitive data will not be captured. Please enable instrumentation to capture sensitive data." + ) + self.enable_console_exporters: bool = data.get("enable_console_exporters") or False self.vs_code_extension_port: int | None = data.get("vs_code_extension_port") self.env_file_path = env_file_path self.env_file_encoding = env_file_encoding self._executed_setup = False + @property + def enable_instrumentation(self) -> bool: + """Whether instrumentation is enabled. + + Always returns False once ``disable_instrumentation()`` has been called, + regardless of the stored value, until ``enable_instrumentation(force=True)`` + clears the sticky disable. + """ + if self._user_disabled: + return False + return self._enable_instrumentation + + @enable_instrumentation.setter + def enable_instrumentation(self, value: bool) -> None: + if self._user_disabled and value: + # Defense in depth: a third-party (or internal) write of True is + # silently dropped while the user-disabled flag is set, so the + # sticky disable cannot be circumvented by direct attribute writes. + logger.debug( + "Ignoring enable_instrumentation=True assignment: instrumentation was explicitly disabled via " + "disable_instrumentation(). Call enable_instrumentation(force=True) to clear the disable." + ) + return + self._enable_instrumentation = value + + @property + def enable_sensitive_data(self) -> bool: + """Whether sensitive-data capture is enabled. + + Always returns False once ``disable_instrumentation()`` has been called. + """ + if self._user_disabled: + return False + return self._enable_sensitive_data + + @enable_sensitive_data.setter + def enable_sensitive_data(self, value: bool) -> None: + if self._user_disabled and value: + logger.debug( + "Ignoring enable_sensitive_data=True assignment: instrumentation was explicitly disabled via " + "disable_instrumentation(). Call enable_sensitive_telemetry(force=True) to clear the disable." + ) + return + self._enable_sensitive_data = value + @property def ENABLED(self) -> bool: """Check if model diagnostics are enabled. @@ -706,6 +770,17 @@ class ObservabilitySettings: """Check if the setup has been executed.""" return self._executed_setup + @property + def is_user_disabled(self) -> bool: + """Whether ``disable_instrumentation()`` has been called and the disable is still in effect. + + Integrations that perform telemetry setup as a side-effect (e.g. provisioning Azure Monitor + providers from a Foundry project's connection string) should consult this flag before doing + their setup work, so the user's explicit opt-out is respected end-to-end and not just at the + framework's span-emission boundary. + """ + return self._user_disabled + def _configure( self, *, @@ -951,24 +1026,91 @@ def _read_int_env(name: str, *, default: int | None = None) -> int | None: return default +def enable_sensitive_telemetry(*, force: bool = False) -> None: + """Enable capture of sensitive data in telemetry for your application. + + Instrumentation is enabled by default; this method exists to opt-in to capturing + sensitive event payloads (e.g., chat messages, tool arguments). + + This method does not configure exporters or providers. It also ensures that + instrumentation is enabled (in case it was explicitly disabled via the + ENABLE_INSTRUMENTATION environment variable). + + Keyword Args: + force: When True, clears any sticky disable previously set by + ``disable_instrumentation()`` before enabling. Without it, calls are + no-ops if instrumentation has been explicitly disabled. + + Warning: + Sensitive events should only be enabled on test and development environments. + """ + global OBSERVABILITY_SETTINGS + if OBSERVABILITY_SETTINGS._user_disabled and not force: # type: ignore[reportPrivateUsage] + logger.info( + "enable_sensitive_telemetry() ignored: instrumentation was explicitly disabled via " + "disable_instrumentation(). Pass force=True to re-enable." + ) + return + if force: + OBSERVABILITY_SETTINGS._user_disabled = False # type: ignore[reportPrivateUsage] + OBSERVABILITY_SETTINGS.enable_instrumentation = True + OBSERVABILITY_SETTINGS.enable_sensitive_data = True + + +def disable_instrumentation() -> None: + """Explicitly disable Agent Framework instrumentation for this process. + + The disable is **sticky**: subsequent attempts by framework auto-setup paths, + library integrations, ``enable_instrumentation()``, ``enable_sensitive_telemetry()``, + ``configure_otel_providers()``, or direct writes to + ``OBSERVABILITY_SETTINGS.enable_instrumentation`` are ignored and no spans, metrics, + or logs are emitted by Agent Framework code paths. + + To override the disable later, call ``enable_instrumentation(force=True)`` or + ``enable_sensitive_telemetry(force=True)``. This makes the user's intent to opt out + win against framework code that would otherwise re-enable instrumentation + automatically. + + Note: + Disabling does not tear down already-configured OpenTelemetry providers, + exporters, or in-flight spans; it gates future captures by Agent Framework + instrumentation only. To stop emitting telemetry from third-party + instrumentations as well, configure them separately. + """ + global OBSERVABILITY_SETTINGS + OBSERVABILITY_SETTINGS._user_disabled = True # type: ignore[reportPrivateUsage] + OBSERVABILITY_SETTINGS._enable_instrumentation = False # type: ignore[reportPrivateUsage] + OBSERVABILITY_SETTINGS._enable_sensitive_data = False # type: ignore[reportPrivateUsage] + + def enable_instrumentation( *, enable_sensitive_data: bool | None = None, + force: bool = False, ) -> None: - """Enable instrumentation for your application. + """Enable instrumentation for Microsoft Agent Framework. - Calling this method implies you want to enable observability in your application. - - This method does not configure exporters or providers. - It only updates the global variables that trigger the instrumentation code. - If you have already set the environment variable ENABLE_INSTRUMENTATION=true, - calling this method has no effect, unless you want to enable or disable sensitive data events. + Note that instrumentation is enabled by default, so this method is only necessary + if you need a programmatic way to enable it (e.g., if you are not sure whether the + environment variable ENABLE_INSTRUMENTATION is set to True or False and want to + ensure it is enabled). Keyword Args: enable_sensitive_data: Enable OpenTelemetry sensitive events. Overrides the environment variable ENABLE_SENSITIVE_DATA if set. Default is None. + force: When True, clears any sticky disable previously set by + ``disable_instrumentation()`` before enabling. Without it, calls are + no-ops if instrumentation has been explicitly disabled. """ global OBSERVABILITY_SETTINGS + if OBSERVABILITY_SETTINGS._user_disabled and not force: # type: ignore[reportPrivateUsage] + logger.info( + "enable_instrumentation() ignored: instrumentation was explicitly disabled via " + "disable_instrumentation(). Pass force=True to re-enable." + ) + return + if force: + OBSERVABILITY_SETTINGS._user_disabled = False # type: ignore[reportPrivateUsage] OBSERVABILITY_SETTINGS.enable_instrumentation = True if enable_sensitive_data is not None: OBSERVABILITY_SETTINGS.enable_sensitive_data = enable_sensitive_data @@ -1008,7 +1150,7 @@ def configure_otel_providers( Since you can only setup one provider per signal type (logs, traces, metrics), you can choose to use this method and take the exporter and provider that we created. Alternatively, you can setup the providers yourself, or through another library - (e.g., Azure Monitor) and just call `enable_instrumentation()` to enable instrumentation. + (e.g., Azure Monitor) and just call `enable_sensitive_telemetry()` to opt-in to sensitive data capture. Note: By default, the Agent Framework emits metrics with the prefixes `agent_framework` @@ -1042,7 +1184,6 @@ def configure_otel_providers( from agent_framework.observability import configure_otel_providers # Using environment variables (recommended) - # Set ENABLE_INSTRUMENTATION=true # Set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 configure_otel_providers() @@ -1087,18 +1228,25 @@ def configure_otel_providers( .. code-block:: python # when azure monitor is installed - from agent_framework.observability import enable_instrumentation + from agent_framework.observability import enable_sensitive_telemetry from azure.monitor.opentelemetry import configure_azure_monitor connection_string = "InstrumentationKey=your_instrumentation_key_here;..." configure_azure_monitor(connection_string=connection_string) - enable_instrumentation() + # Optional: opt into capturing sensitive data + enable_sensitive_telemetry() References: - https://opentelemetry.io/docs/languages/sdk-configuration/general/ - https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ """ global OBSERVABILITY_SETTINGS + if OBSERVABILITY_SETTINGS._user_disabled: # type: ignore[reportPrivateUsage] + logger.info( + "configure_otel_providers(): instrumentation was explicitly disabled via " + "disable_instrumentation(); providers and exporters will still be configured but " + "Agent Framework will emit no telemetry until enable_instrumentation(force=True) is called." + ) if env_file_path: # Build kwargs, excluding None values settings_kwargs: dict[str, Any] = { @@ -1280,7 +1428,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): if stream: span = _start_streaming_span(attributes, OtelAttr.REQUEST_MODEL) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1344,6 +1492,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and isinstance(response, ChatResponse) and response.messages + and span.is_recording() ): _capture_messages( span=span, @@ -1374,7 +1523,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): async def _get_response() -> ChatResponse: with _get_span(attributes=attributes, span_name_attribute=OtelAttr.REQUEST_MODEL) as span: - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1408,7 +1557,7 @@ class ChatTelemetryLayer(Generic[OptionsCoT]): duration=duration, ) _mark_inner_response_telemetry_captured(response) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages and span.is_recording(): finish_reason = cast( "FinishReason | None", response.finish_reason if response.finish_reason in FINISH_REASON_MAP else None, @@ -1552,7 +1701,7 @@ class AgentTelemetryLayer: if stream: span = _start_streaming_span(attributes, OtelAttr.AGENT_NAME) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1613,6 +1762,7 @@ class AgentTelemetryLayer: OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and isinstance(response, AgentResponse) and response.messages + and span.is_recording() ): _capture_messages( span=span, @@ -1645,7 +1795,7 @@ class AgentTelemetryLayer: async def _run() -> AgentResponse[Any]: try: with _get_span(attributes=attributes, span_name_attribute=OtelAttr.AGENT_NAME) as span: - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, @@ -1669,7 +1819,7 @@ class AgentTelemetryLayer: ) _apply_accumulated_usage(response_attributes, inner_response_telemetry_captured_fields) _capture_response(span=span, attributes=response_attributes, duration=duration) - if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages: + if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and response.messages and span.is_recording(): _capture_messages( span=span, provider_name=provider_name, diff --git a/python/packages/core/tests/core/test_observability.py b/python/packages/core/tests/core/test_observability.py index 71b59a351b..6185bccd44 100644 --- a/python/packages/core/tests/core/test_observability.py +++ b/python/packages/core/tests/core/test_observability.py @@ -1015,11 +1015,25 @@ def test_observability_settings_is_setup_initial(monkeypatch): assert settings.is_setup is False -# region Test enable_instrumentation function +def test_enable_sensitive_telemetry_function(monkeypatch): + """Test enable_sensitive_telemetry function enables instrumentation.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false") + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True def test_enable_instrumentation_function(monkeypatch): - """Test enable_instrumentation function enables instrumentation.""" + """Test enable_instrumentation function enables instrumentation when disabled via env.""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") @@ -1032,10 +1046,12 @@ def test_enable_instrumentation_function(monkeypatch): observability.enable_instrumentation() assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + # Sensitive data should remain False when not explicitly enabled + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False def test_enable_instrumentation_with_sensitive_data(monkeypatch): - """Test enable_instrumentation function with sensitive_data parameter.""" + """Test enable_instrumentation function with explicit sensitive_data parameter.""" import importlib monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") @@ -1049,111 +1065,6 @@ def test_enable_instrumentation_with_sensitive_data(monkeypatch): assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True -def test_enable_instrumentation_reads_env_sensitive_data(monkeypatch): - """Test enable_instrumentation re-reads ENABLE_SENSITIVE_DATA from os.environ when not explicitly passed.""" - import importlib - - monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false") - - observability = importlib.import_module("agent_framework.observability") - importlib.reload(observability) - - assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False - - # Simulate load_dotenv() setting env var after import - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") - - observability.enable_instrumentation() - assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True - assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True - - -def test_configure_otel_providers_reads_env_sensitive_data(monkeypatch): - """Test configure_otel_providers re-reads ENABLE_SENSITIVE_DATA from os.environ when not explicitly passed.""" - import importlib - - monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false") - monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) - monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) - for key in [ - "OTEL_EXPORTER_OTLP_ENDPOINT", - "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", - "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", - "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", - ]: - monkeypatch.delenv(key, raising=False) - - observability = importlib.import_module("agent_framework.observability") - importlib.reload(observability) - - assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False - - # Simulate load_dotenv() setting env var after import - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") - - with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): - observability.configure_otel_providers() - assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True - assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True - - -def test_configure_otel_providers_reads_env_vs_code_port(monkeypatch): - """Test configure_otel_providers re-reads VS_CODE_EXTENSION_PORT from os.environ when not explicitly passed.""" - import importlib - from unittest.mock import patch as mock_patch - - monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") - monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) - monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) - for key in [ - "OTEL_EXPORTER_OTLP_ENDPOINT", - "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", - "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", - "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", - ]: - monkeypatch.delenv(key, raising=False) - - observability = importlib.import_module("agent_framework.observability") - importlib.reload(observability) - - assert observability.OBSERVABILITY_SETTINGS.vs_code_extension_port is None - - # Simulate load_dotenv() setting env var after import - monkeypatch.setenv("VS_CODE_EXTENSION_PORT", "4317") - - # Mock _configure to avoid needing optional OTLP gRPC exporter dependency - with mock_patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): - observability.configure_otel_providers() - assert observability.OBSERVABILITY_SETTINGS.vs_code_extension_port == 4317 - - -def test_configure_otel_providers_explicit_param_overrides_env(monkeypatch): - """Test that explicit parameters to configure_otel_providers override env vars.""" - import importlib - - monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") - monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") - monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) - monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) - for key in [ - "OTEL_EXPORTER_OTLP_ENDPOINT", - "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", - "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", - "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", - ]: - monkeypatch.delenv(key, raising=False) - - observability = importlib.import_module("agent_framework.observability") - importlib.reload(observability) - - # Explicit False should override the env var True - with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): - observability.configure_otel_providers(enable_sensitive_data=False) - assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False - - def test_enable_instrumentation_explicit_param_overrides_env(monkeypatch): """Test that explicit enable_sensitive_data parameter to enable_instrumentation overrides env var.""" import importlib @@ -1269,6 +1180,161 @@ def test_enable_instrumentation_preserves_console_exporters_after_env_removed(mo assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True +def test_configure_otel_providers_reads_env_sensitive_data(monkeypatch): + """Test configure_otel_providers re-reads ENABLE_SENSITIVE_DATA from os.environ when not explicitly passed.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false") + monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + for key in [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ]: + monkeypatch.delenv(key, raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + # Simulate load_dotenv() setting env var after import + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") + + with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + + +def test_configure_otel_providers_reads_env_vs_code_port(monkeypatch): + """Test configure_otel_providers re-reads VS_CODE_EXTENSION_PORT from os.environ when not explicitly passed.""" + import importlib + from unittest.mock import patch as mock_patch + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + for key in [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ]: + monkeypatch.delenv(key, raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.vs_code_extension_port is None + + # Simulate load_dotenv() setting env var after import + monkeypatch.setenv("VS_CODE_EXTENSION_PORT", "4317") + + # Mock _configure to avoid needing optional OTLP gRPC exporter dependency + with mock_patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers() + assert observability.OBSERVABILITY_SETTINGS.vs_code_extension_port == 4317 + + +def test_configure_otel_providers_explicit_param_overrides_env(monkeypatch): + """Test that explicit parameters to configure_otel_providers override env vars.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") + monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + for key in [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ]: + monkeypatch.delenv(key, raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + # Explicit False should override the env var True + with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers(enable_sensitive_data=False) + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_enable_sensitive_telemetry_does_not_touch_console_exporters(monkeypatch): + """Test enable_sensitive_telemetry does not modify enable_console_exporters (it is an exporter concern).""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False + + # Simulate load_dotenv() setting env var after import + monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true") + + observability.enable_sensitive_telemetry() + # enable_console_exporters is not managed by enable_sensitive_telemetry; + # it is only read by configure_otel_providers. + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False + + +def test_enable_sensitive_telemetry_does_not_clobber_console_exporters(monkeypatch): + """Test enable_sensitive_telemetry does not reset enable_console_exporters set by prior configure call.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False) + for key in [ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + ]: + monkeypatch.delenv(key, raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + # Set console exporters via configure_otel_providers + with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers(enable_console_exporters=True) + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + # Calling enable_sensitive_telemetry should not clobber the value + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + +def test_enable_sensitive_telemetry_preserves_console_exporters_after_env_removed(monkeypatch): + """Test enable_sensitive_telemetry preserves enable_console_exporters when env var is removed after reload.""" + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true") + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + # Remove the env var after reload + monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False) + + # enable_sensitive_telemetry should not reset the value + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True + + def test_configure_otel_providers_reads_env_console_exporters(monkeypatch): """Test configure_otel_providers re-reads ENABLE_CONSOLE_EXPORTERS from os.environ when not explicitly passed.""" import importlib @@ -1321,6 +1387,189 @@ def test_configure_otel_providers_explicit_console_exporters_overrides_env(monke assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False +# region Test default-on instrumentation + + +def test_observability_settings_defaults_instrumentation_true(monkeypatch): + """ENABLE_INSTRUMENTATION unset → ObservabilitySettings defaults to True.""" + from agent_framework.observability import ObservabilitySettings + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + settings = ObservabilitySettings() + assert settings.enable_instrumentation is True + + +def test_enable_instrumentation_reads_env_sensitive_data(monkeypatch): + """No-arg enable_instrumentation() re-reads ENABLE_SENSITIVE_DATA from env at call time. + + Covers the fallback branch where the env var is set AFTER import (e.g. via load_dotenv()). + """ + import importlib + + monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false") + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + # Simulate load_dotenv() setting the env var after import + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") + observability.enable_instrumentation() + + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + + +# region Test disable_instrumentation sticky behavior + + +def test_disable_instrumentation_flips_settings_off(monkeypatch): + """disable_instrumentation() immediately turns instrumentation and sensitive data off.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true") + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + assert observability.OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED is True + + observability.disable_instrumentation() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + assert observability.OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED is False + assert observability.OBSERVABILITY_SETTINGS.ENABLED is False + + +def test_disable_instrumentation_is_sticky_against_enable_instrumentation(monkeypatch): + """Sticky disable: enable_instrumentation() without force is a no-op after disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_instrumentation(enable_sensitive_data=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_disable_instrumentation_is_sticky_against_enable_sensitive_telemetry(monkeypatch): + """Sticky disable: enable_sensitive_telemetry() without force is a no-op after disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_sensitive_telemetry() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_disable_instrumentation_is_sticky_against_configure_otel_providers(monkeypatch): + """Sticky disable: configure_otel_providers() does not flip instrumentation back on.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + with patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"): + observability.configure_otel_providers(enable_sensitive_data=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_disable_instrumentation_intercepts_direct_attribute_writes(monkeypatch): + """Sticky disable: direct OBSERVABILITY_SETTINGS.enable_instrumentation = True is intercepted.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.OBSERVABILITY_SETTINGS.enable_instrumentation = True + observability.OBSERVABILITY_SETTINGS.enable_sensitive_data = True + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False + + +def test_enable_instrumentation_force_clears_disable(monkeypatch): + """enable_instrumentation(force=True) clears the sticky disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_instrumentation(force=True, enable_sensitive_data=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + + +def test_enable_sensitive_telemetry_force_clears_disable(monkeypatch): + """enable_sensitive_telemetry(force=True) clears the sticky disable.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_sensitive_telemetry(force=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True + + +def test_disable_instrumentation_persists_after_force_until_redisabled(monkeypatch): + """After force-enable then disable again, the sticky disable is re-armed.""" + import importlib + + monkeypatch.delenv("ENABLE_INSTRUMENTATION", raising=False) + monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False) + + observability = importlib.import_module("agent_framework.observability") + importlib.reload(observability) + + observability.disable_instrumentation() + observability.enable_instrumentation(force=True) + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True + + observability.disable_instrumentation() + observability.enable_instrumentation() + assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is False + + +def test_disable_instrumentation_in_all(monkeypatch): + """disable_instrumentation must be re-exported from the module's __all__.""" + import agent_framework.observability as observability + + assert "disable_instrumentation" in observability.__all__ + assert callable(observability.disable_instrumentation) + + # region Test _to_otel_part content types @@ -3797,3 +4046,135 @@ async def test_agent_streaming_execute_failure_closes_span_and_resets_contextvar agent_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION] assert len(agent_spans) == 1 assert agent_spans[0].status.status_code == StatusCode.ERROR + + +# region Test heavy operations skipped when span is not recording +# +# When ``ENABLE_INSTRUMENTATION`` is on (the default) but no OpenTelemetry +# tracer provider has been configured, the global provider is the +# ``ProxyTracerProvider`` which returns non-recording spans. The telemetry +# layers gate sensitive-data serialization (``_capture_messages``) on +# ``span.is_recording()`` so that we don't pay the JSON-serialization cost +# when the span is going to be dropped anyway. The tests below verify that +# behavior by patching ``get_tracer`` to return a ``NoOpTracer``. + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_chat_capture_messages_skipped_when_span_not_recording( + mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Heavy message serialization is skipped when no provider is configured (non-streaming).""" + from opentelemetry.trace import NoOpTracer + + client = mock_chat_client() + messages = [Message(role="user", contents=["Test"])] + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + response = await client.get_response(messages=messages, options={"model": "Test"}) + + assert response is not None + # Sensitive-data serialization must be skipped because span.is_recording() is False. + assert mock_capture_messages.call_count == 0 + # _capture_response still runs so that metric histograms continue to record. + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_chat_streaming_capture_messages_skipped_when_span_not_recording( + mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Heavy message serialization is skipped when no provider is configured (streaming).""" + from opentelemetry.trace import NoOpTracer + + client = mock_chat_client() + messages = [Message(role="user", contents=["Test"])] + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + updates: list[ChatResponseUpdate] = [] + stream = client.get_response(messages=messages, stream=True, options={"model": "Test"}) + async for update in stream: + updates.append(update) + await stream.get_final_response() + + assert len(updates) == 2 + assert mock_capture_messages.call_count == 0 + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_agent_capture_messages_skipped_when_span_not_recording( + mock_chat_agent, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Agent heavy serialization is skipped when no provider is configured (non-streaming).""" + from opentelemetry.trace import NoOpTracer + + agent = mock_chat_agent() + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + response = await agent.run("Test message") + + assert response is not None + assert mock_capture_messages.call_count == 0 + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_agent_streaming_capture_messages_skipped_when_span_not_recording( + mock_chat_agent, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Agent heavy serialization is skipped when no provider is configured (streaming).""" + from opentelemetry.trace import NoOpTracer + + agent = mock_chat_agent() + span_exporter.clear() + + with ( + patch("agent_framework.observability.get_tracer", return_value=NoOpTracer()), + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + updates: list[Any] = [] + stream = agent.run("Test message", stream=True) + async for update in stream: + updates.append(update) + await stream.get_final_response() + + assert len(updates) == 2 + assert mock_capture_messages.call_count == 0 + assert mock_capture_response.call_count == 1 + + +@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) +async def test_chat_capture_messages_called_when_span_recording( + mock_chat_client, span_exporter: InMemorySpanExporter, enable_sensitive_data +): + """Sanity check: with a real recording provider, sensitive-data capture still runs.""" + client = mock_chat_client() + messages = [Message(role="user", contents=["Test"])] + span_exporter.clear() + + with ( + patch("agent_framework.observability._capture_messages") as mock_capture_messages, + patch("agent_framework.observability._capture_response") as mock_capture_response, + ): + response = await client.get_response(messages=messages, options={"model": "Test"}) + + assert response is not None + # Two _capture_messages calls: one for input, one for output messages. + assert mock_capture_messages.call_count == 2 + assert mock_capture_response.call_count == 1 diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 8b737694e3..056d3977af 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -793,8 +793,22 @@ class RawFoundryAgent( # type: ignore[misc] Raises: ImportError: If azure-monitor-opentelemetry-exporter is not installed. """ + from agent_framework.observability import ( + OBSERVABILITY_SETTINGS, + create_metric_views, + create_resource, + enable_instrumentation, + ) from azure.core.exceptions import ResourceNotFoundError + if OBSERVABILITY_SETTINGS.is_user_disabled: + logger.info( + "FoundryAgent.configure_azure_monitor(): Skipping setup because instrumentation was " + "explicitly disabled via disable_instrumentation(). Call enable_instrumentation(force=True) " + "to re-enable, then re-invoke configure_azure_monitor()." + ) + return + client = self.client if not isinstance(client, RawFoundryAgentChatClient): raise TypeError("configure_azure_monitor requires a RawFoundryAgentChatClient-based client.") @@ -817,8 +831,6 @@ class RawFoundryAgent( # type: ignore[misc] "Install it with: pip install azure-monitor-opentelemetry" ) from exc - from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation - if "resource" not in kwargs: kwargs["resource"] = create_resource() diff --git a/python/packages/foundry/agent_framework_foundry/_chat_client.py b/python/packages/foundry/agent_framework_foundry/_chat_client.py index 614efcad15..7f8e033036 100644 --- a/python/packages/foundry/agent_framework_foundry/_chat_client.py +++ b/python/packages/foundry/agent_framework_foundry/_chat_client.py @@ -271,8 +271,22 @@ class RawFoundryChatClient( # type: ignore[misc] Raises: ImportError: If azure-monitor-opentelemetry-exporter is not installed. """ + from agent_framework.observability import ( + OBSERVABILITY_SETTINGS, + create_metric_views, + create_resource, + enable_instrumentation, + ) from azure.core.exceptions import ResourceNotFoundError + if OBSERVABILITY_SETTINGS.is_user_disabled: + logger.info( + "FoundryChatClient.configure_azure_monitor(): Skipping setup because instrumentation was " + "explicitly disabled via disable_instrumentation(). Call enable_instrumentation(force=True) " + "to re-enable, then re-invoke configure_azure_monitor()." + ) + return + try: conn_string = await self.project_client.telemetry.get_application_insights_connection_string() except ResourceNotFoundError: @@ -291,8 +305,6 @@ class RawFoundryChatClient( # type: ignore[misc] "Install it with: pip install azure-monitor-opentelemetry" ) from exc - from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation - if "resource" not in kwargs: kwargs["resource"] = create_resource() diff --git a/python/samples/02-agents/observability/.env.example b/python/samples/02-agents/observability/.env.example index f3dd329bac..c43f8f239c 100644 --- a/python/samples/02-agents/observability/.env.example +++ b/python/samples/02-agents/observability/.env.example @@ -27,6 +27,9 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317" # Agent Framework specific settings # ================================== +# Observability is enabled by default. Set to "false" to opt out. +# ENABLE_INSTRUMENTATION=false + # Enable sensitive data logging (prompts, responses, etc.) # WARNING: Only enable in dev/test environments ENABLE_SENSITIVE_DATA=true @@ -34,9 +37,6 @@ ENABLE_SENSITIVE_DATA=true # Optional: Enable console exporters for debugging # ENABLE_CONSOLE_EXPORTERS=true -# Optional: Enable observability (automatically enabled if env vars are set or configure_otel_providers() is called) -# ENABLE_INSTRUMENTATION=true - # OpenAI specific variables # ========================== OPENAI_API_KEY="..." diff --git a/python/samples/02-agents/observability/README.md b/python/samples/02-agents/observability/README.md index b2fbf7400f..d6a251eef5 100644 --- a/python/samples/02-agents/observability/README.md +++ b/python/samples/02-agents/observability/README.md @@ -1,12 +1,12 @@ # Agent Framework Observability -This sample folder shows how a Python application can be configured to send Agent Framework observability data to the Application Performance Management (APM) vendor(s) of your choice based on the OpenTelemetry standard. +These samples show how to send Agent Framework observability data to the Application Performance Management (APM) backend of your choice, based on the OpenTelemetry standard. -In this sample, we provide options to send telemetry to [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview), [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash) and the console. +The samples target [Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview), the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash), and the console, but any OTLP-compatible backend works. -> **Quick Start**: For local development without Azure setup, you can use the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone) which runs locally via Docker and provides an excellent telemetry viewing experience for OpenTelemetry data. Or you can use the built-in tracing module of the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio). +> **Quick Start**: For local development without Azure setup, use the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone) (runs locally via Docker), or the built-in tracing module of the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio). -> Note that it is also possible to use other Application Performance Management (APM) vendors. An example is [Prometheus](https://prometheus.io/docs/introduction/overview/). Please refer to this [page](https://opentelemetry.io/docs/languages/python/exporters/) to learn more about exporters. +> Other backends such as [Prometheus](https://prometheus.io/docs/introduction/overview/) are also supported. See the [OpenTelemetry Python exporters](https://opentelemetry.io/docs/languages/python/exporters/) page for the full list. For more information, please refer to the following resources: @@ -18,19 +18,15 @@ For more information, please refer to the following resources: ## What to expect -The Agent Framework Python SDK is designed to efficiently generate comprehensive logs, traces, and metrics throughout the flow of agent/model invocation and tool execution. This allows you to effectively monitor your AI application's performance and accurately track token consumption. It does so based on the Semantic Conventions for GenAI defined by OpenTelemetry, and the workflows emit their own spans to provide end-to-end visibility. +The Agent Framework Python SDK is **natively instrumented** to emit logs, traces, and metrics throughout agent/model invocation and tool execution, so you can monitor your AI application's performance and track token consumption. Instrumentation follows the OpenTelemetry [Semantic Conventions for GenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/), and workflows emit their own spans for end-to-end visibility. -Next to what happens in the code when you run, we also make setting up observability as easy as possible. By calling a single function `configure_otel_providers()` from the `agent_framework.observability` module, you can enable telemetry for traces, logs, and metrics. The function automatically reads standard OpenTelemetry environment variables to configure exporters and providers, making it simple to get started. - -### MCP trace propagation - -Whenever there is an active OpenTelemetry span context, Agent Framework automatically propagates trace context to MCP servers via the `params._meta` field of `tools/call` requests. It uses the globally-configured OpenTelemetry propagator(s) (W3C Trace Context by default, producing `traceparent` and `tracestate`), so custom propagators (B3, Jaeger, etc.) are also supported. This enables distributed tracing across agent-to-MCP-server boundaries, compliant with the [MCP `_meta` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic#_meta). - -**Scope:** automatic `_meta` injection applies only to MCP sessions that the agent process itself opens — `MCPStreamableHTTPTool`, `MCPStdioTool`, and `MCPWebsocketTool` (or any other client-opened `MCPTool` subclass). It does **not** apply to hosted/provider-managed MCP tool configurations such as `FoundryChatClient.get_mcp_tool(...)`, `OpenAIChatClient.get_mcp_tool(...)`, `AnthropicClient.get_mcp_tool(...)`, `GeminiChatClient.get_mcp_tool(...)`, or toolbox-fetched tools (for example, `toolbox = await client.get_toolbox(...)`, then passing `toolbox.tools` into `Agent(tools=...)`), because in those cases the `tools/call` message is issued by the provider service runtime rather than by the agent process. As a result, the framework has no opportunity to inject trace context into those requests, and propagating `traceparent`/`tracestate` across that hosted-service boundary is the responsibility of the service runtime, not Agent Framework. If end-to-end distributed tracing to the downstream MCP server is required, use a client-opened MCP transport instead of a hosted connector. +Setting up observability is also easy: a single call to `configure_otel_providers()` from the `agent_framework.observability` module wires up the trace, log, and metric providers. It reads the standard OpenTelemetry environment variables to configure exporters automatically. ### Five patterns for configuring observability -We've identified multiple ways to configure observability in your application, depending on your needs: +> Setting up observability has two parts: (1) **instrumentation**, the code that generates telemetry, and (2) **exporter/provider configuration**, which decides where that telemetry is sent. Agent Framework is natively instrumented and **enabled by default**, so you only need to handle the second part. + +There are five common ways to do that, depending on your needs: **1. Standard otel environment variables, configured for you** @@ -42,22 +38,29 @@ from agent_framework.observability import configure_otel_providers # Reads OTEL_EXPORTER_OTLP_* environment variables automatically configure_otel_providers() ``` + Or if you just want console exporters: + ```python from agent_framework.observability import configure_otel_providers -# Enable console exporters via environment variable configure_otel_providers(enable_console_exporters=True) +# It is also possible to set ENABLE_CONSOLE_EXPORTERS=true in environment +# variables instead of calling `configure_otel_providers()` with the parameter. +# The framework will automatically read that and set up console exporters. ``` + This is the **recommended approach** for getting started. **2. Custom Exporters** -One level more control over the exporters that are created is to do that yourself, and then pass them to `configure_otel_providers()`. We will still create the providers for you, but you can customize the exporters as needed: + +For more control, construct exporters yourself and pass them to `configure_otel_providers()`. The framework still creates the providers for you: ```python from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.exporter.otlp.proto.grpc.exporter import Compression from agent_framework.observability import configure_otel_providers # Create custom exporters with specific configuration @@ -67,17 +70,17 @@ exporters = [ OTLPMetricExporter(endpoint="http://localhost:4317"), ] -# These will be added alongside any exporters from environment variables -configure_otel_providers(exporters=exporters, enable_sensitive_data=True) +# These are added alongside any exporters configured from environment variables +configure_otel_providers(exporters=exporters) ``` -**3. Third party setup** +**3. Third-party setup** -A lot of third party specific otel package, have their own easy setup methods, for example Azure Monitor has `configure_azure_monitor()`. You can use those methods to setup the third party first, and then call `enable_instrumentation()` from the `agent_framework.observability` module to activate the Agent Framework telemetry code paths. In all these cases, if you already setup observability via environment variables, you don't need to call `enable_instrumentation()` as it will be enabled automatically. +Many third-party OTel packages ship their own setup helpers (for example, Azure Monitor's `configure_azure_monitor()`). You can use those directly — Agent Framework instrumentation is on by default, so no extra wiring is needed. To also capture sensitive data, call `enable_sensitive_telemetry()` from `agent_framework.observability`. ```python from azure.monitor.opentelemetry import configure_azure_monitor -from agent_framework.observability import create_resource, enable_instrumentation +from agent_framework.observability import create_resource, enable_sensitive_telemetry # Configure Azure Monitor first configure_azure_monitor( @@ -86,10 +89,10 @@ configure_azure_monitor( enable_live_metrics=True, ) -# Then activate Agent Framework's telemetry code paths -# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars -enable_instrumentation(enable_sensitive_data=False) +# Optional: opt in to capturing sensitive data +enable_sensitive_telemetry() ``` + For Microsoft Foundry projects, use `client.configure_azure_monitor()` which retrieves the connection string from the project and configures everything: ```python @@ -110,7 +113,7 @@ Or with [Langfuse](https://langfuse.com/integrations/frameworks/microsoft-agent- ```python # environment should be setup correctly, with langfuse urls and keys -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry from langfuse import get_client langfuse = get_client() @@ -121,9 +124,9 @@ if langfuse.auth_check(): else: print("Authentication failed. Please check your credentials and host.") -# Then activate Agent Framework's telemetry code paths -# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars -enable_instrumentation(enable_sensitive_data=False) +# Agent Framework instrumentation is on by default. +# Optional: opt in to capturing sensitive data +enable_sensitive_telemetry() ``` Or with [Comet Opik](https://www.comet.com/docs/opik/integrations/microsoft-agent-framework): @@ -131,53 +134,152 @@ Or with [Comet Opik](https://www.comet.com/docs/opik/integrations/microsoft-agen ```python import os -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry # Use Opik OTLP settings from your project settings os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = "" os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = "" -# Then activate Agent Framework's telemetry code paths -# This is optional if ENABLE_INSTRUMENTATION and or ENABLE_SENSITIVE_DATA are set in env vars -enable_instrumentation(enable_sensitive_data=False) +# Agent Framework instrumentation is on by default. +# Optional: opt in to capturing sensitive data +enable_sensitive_telemetry() ``` **4. Manual setup** -Of course you can also do a complete manual setup of exporters, providers, and instrumentation. Please refer to sample [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a comprehensive example of how to manually setup exporters and providers for traces, logs, and metrics that will get sent to the console. This gives you full control over which exporters and providers to use. We do have a helper function `create_resource()` in the `agent_framework.observability` module that you can use to create a resource with the appropriate service name and version based on environment variables or standard defaults for Agent Framework, this is not used in the sample. -**5. Auto-instrumentation (zero-code)** -You can also use the [OpenTelemetry CLI tool](https://opentelemetry.io/docs/instrumentation/python/getting-started/#automatic-instrumentation) to automatically instrument your application without changing any code. Please refer to sample [advanced_zero_code.py](./advanced_zero_code.py) for an example of how to use the CLI tool to enable instrumentation for Agent Framework applications. +For full control, set up providers and exporters yourself. See [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) for a complete example that sends traces, logs, and metrics to the console. The `create_resource()` helper in `agent_framework.observability` can build a resource with the appropriate service name and version from environment variables (or sensible defaults), although the sample does not use it. + +**5. Zero-code provider/exporter configuration** + +Because Agent Framework is **natively instrumented** with OpenTelemetry, you do not need to auto-instrument the framework itself. You can, however, use the [`opentelemetry-instrument`](https://opentelemetry.io/docs/zero-code/python/) CLI wrapper to configure the global tracer/meter providers and exporters from environment variables (or CLI flags) at process startup. Your application code then does not need to call `configure_otel_providers()` — the native spans and metrics from Agent Framework are picked up by the globally configured pipeline. See [advanced_zero_code.py](./advanced_zero_code.py) for an example. + +### MCP trace propagation + +Whenever there is an active OpenTelemetry span context, Agent Framework automatically propagates trace context to MCP servers via the `params._meta` field of `tools/call` requests. It uses the globally configured OpenTelemetry propagator(s) — W3C Trace Context by default (producing `traceparent` and `tracestate`) — so custom propagators (B3, Jaeger, etc.) are also supported. This enables distributed tracing across agent-to-MCP-server boundaries, compliant with the [MCP `_meta` specification](https://modelcontextprotocol.io/specification/2025-11-25/basic#_meta). + +**Scope:** automatic `_meta` injection applies only to MCP sessions that the agent process itself opens — `MCPStreamableHTTPTool`, `MCPStdioTool`, and `MCPWebsocketTool` (or any other client-opened `MCPTool` subclass). It does **not** apply to hosted or provider-managed MCP tool configurations such as `FoundryChatClient.get_mcp_tool(...)`, `OpenAIChatClient.get_mcp_tool(...)`, `AnthropicClient.get_mcp_tool(...)`, `GeminiChatClient.get_mcp_tool(...)`, or toolbox-fetched tools (e.g. `toolbox = await client.get_toolbox(...)` then `Agent(tools=toolbox.tools)`). In those cases the `tools/call` message is issued by the provider service runtime rather than by the agent process, so propagating `traceparent`/`tracestate` across that boundary is the service runtime's responsibility. If you need end-to-end distributed tracing to the downstream MCP server, use a client-opened MCP transport instead of a hosted connector. ## Configuration ### Dependencies -As part of Agent Framework we use the following OpenTelemetry packages: -- `opentelemetry-api` -- `opentelemetry-sdk` -- `opentelemetry-semantic-conventions-ai` +Agent Framework's core depends on **`opentelemetry-api`** only — the API package is enough for the instrumentation hooks (spans, meters, log records) to emit telemetry, and it has no runtime side effects when no provider is configured. -We do not install exporters by default, so you will need to add those yourself, this prevents us from installing unnecessary dependencies. For Application Insights, you will need to install `azure-monitor-opentelemetry`. For Aspire Dashboard or other OTLP compatible backends, you will need to install `opentelemetry-exporter-otlp-proto-grpc`. For HTTP protocol support, you will also need to install `opentelemetry-exporter-otlp-proto-http`. +If you want the framework to set up providers / exporters for you via `configure_otel_providers()` (or to use the `create_resource()` / `create_metric_views()` helpers), you also need the OpenTelemetry SDK: -And for many others, different packages are used, so refer to the documentation of the specific exporter you want to use. +```bash +pip install opentelemetry-sdk +``` + +If `opentelemetry-sdk` is missing, those helper functions raise a clear `ImportError` telling you to install it. Day-to-day instrumentation still works without the SDK as long as some other component (e.g. `azure-monitor-opentelemetry`, your application bootstrap, an APM agent) has configured the global OpenTelemetry providers. + +Exporters are **not** installed by default — install only what you need: +- **Application Insights**: `azure-monitor-opentelemetry` +- **Aspire Dashboard or other OTLP/gRPC backends**: `opentelemetry-exporter-otlp-proto-grpc` +- **OTLP over HTTP**: `opentelemetry-exporter-otlp-proto-http` + +For other backends, refer to the documentation of the specific exporter. ### Environment variables -The following environment variables are used to turn on/off observability of the Agent Framework: +Agent Framework reads the following environment variables: -- `ENABLE_INSTRUMENTATION` -- `ENABLE_SENSITIVE_DATA` -- `ENABLE_CONSOLE_EXPORTERS` +| Variable | Default | Purpose | +|----------|---------|---------| +| `ENABLE_INSTRUMENTATION` | `true` | Set to `false` to disable native instrumentation. See [Disabling instrumentation](#disabling-instrumentation) for the programmatic alternative with sticky semantics. | +| `ENABLE_SENSITIVE_DATA` | `false` | Set to `true` to emit sensitive data (prompts, responses, etc.). | +| `ENABLE_CONSOLE_EXPORTERS` | `false` | Set to `true` to add console exporters. Only used by `configure_otel_providers()`. | +| `VS_CODE_EXTENSION_PORT` | unset | Port used by the [AI Toolkit for VS Code](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio#tracing) tracing integration. Only used by `configure_otel_providers()`. | -All of these are booleans and default to `false`. +You can also call `enable_sensitive_telemetry()` from `agent_framework.observability` to opt in to sensitive-data capture programmatically. -Finally we have `VS_CODE_EXTENSION_PORT` which you can set to a port, which can be used to setup the AI Toolkit for VS Code tracing integration. See [here](https://marketplace.visualstudio.com/items?itemName=ms-windows-ai-studio.windows-ai-studio#tracing) for more details. +> **Note**: Sensitive data includes prompts, responses, and tool arguments. Only enable it in development or test environments — it may expose user or system secrets in production. -The framework will emit observability data when the `ENABLE_INSTRUMENTATION` environment variable is set to `true`. If both are `true` then it will also emit sensitive information. When these are not set, or set to false, you can use the `enable_instrumentation()` function from the `agent_framework.observability` module to turn on instrumentation programmatically. This is useful when you want to control this via code instead of environment variables. +### Disabling instrumentation -> **Note**: Sensitive information includes prompts, responses, and more, and should only be enabled in a development or test environment. It is not recommended to enable this in production environments as it may expose sensitive data. +There are two ways to turn Agent Framework's native instrumentation off, and they have **different scopes**: -The two other variables, `ENABLE_CONSOLE_EXPORTERS` and `VS_CODE_EXTENSION_PORT`, are used to configure where the observability data is sent. Those are only activated when calling `configure_otel_providers()`. +| Approach | Scope | Sticky? | When framework code calls `enable_instrumentation()` later, what happens? | +|----------|-------|---------|---------------------------------------------------------------------------| +| `ENABLE_INSTRUMENTATION=false` in the environment | Initial settings only | No | Instrumentation flips back **on**. | +| `disable_instrumentation()` called from code | Process-wide, sticky | Yes | Instrumentation **stays off** — the user-disable intent wins. | + +If you want telemetry off **and want it to stay off**, use `disable_instrumentation()`. + +#### Sticky semantics — why this matters + +Framework integrations and third-party libraries can call `enable_instrumentation()`, `enable_sensitive_telemetry()`, or `configure_otel_providers()` as part of their own setup. For example, `FoundryChatClient.configure_azure_monitor()` calls `enable_instrumentation()` after wiring up Azure Monitor. That's normally what you want — but if **you** have explicitly opted out, you don't want any of those calls to silently re-enable telemetry. + +`disable_instrumentation()` solves this by setting a **sticky** flag on `OBSERVABILITY_SETTINGS` that remains in effect until you explicitly clear it. While the flag is set: + +1. `OBSERVABILITY_SETTINGS.enable_instrumentation` and `enable_sensitive_data` **read as `False`** regardless of the stored value. +2. `enable_instrumentation()` and `enable_sensitive_telemetry()` are **no-ops** and log an info-level message. +3. `configure_otel_providers()` still configures providers / exporters / views (so a later force-enable can use them), but does not flip instrumentation on. +4. Direct attribute writes like `OBSERVABILITY_SETTINGS.enable_instrumentation = True` from any code are **silently dropped** (defense in depth). +5. Integrations that consult `OBSERVABILITY_SETTINGS.is_user_disabled` (e.g. `FoundryChatClient.configure_azure_monitor()`, `FoundryAgent.configure_azure_monitor()`) **skip their setup entirely**, so global Azure Monitor providers aren't installed unnecessarily. + +```python +from agent_framework.observability import disable_instrumentation + +# After this call, Agent Framework expresses your intent to opt out of telemetry. +# Library and framework code is expected to honor that intent and not flip +# instrumentation back on (e.g. by calling `enable_instrumentation()`, +# `enable_sensitive_telemetry()`, or writing to public attributes on +# `OBSERVABILITY_SETTINGS`). The framework actively short-circuits the public +# enable paths so the user's intent stays leading. A determined caller can still +# pass `force=True` or mutate private (`_`-prefixed) attributes to bypass it, +# but those are out-of-contract escape hatches that should not be used by +# integrations on the user's behalf. +disable_instrumentation() +``` + +#### Forcing re-enablement after a disable + +To intentionally re-enable telemetry after `disable_instrumentation()`, pass `force=True` to either of the two public enable helpers. This is the only way to clear the sticky disable, so the user's opt-out can only be reversed by a deliberate user opt-in: + +```python +from agent_framework.observability import ( + disable_instrumentation, + enable_instrumentation, + enable_sensitive_telemetry, +) + +disable_instrumentation() + +# Without force=True, these are no-ops while the disable is sticky: +enable_instrumentation() # logs info, does nothing +enable_sensitive_telemetry() # logs info, does nothing + +# With force=True, the sticky disable is cleared and the call proceeds: +enable_instrumentation(force=True) +# or +enable_sensitive_telemetry(force=True) + +# After a force-enable you can `disable_instrumentation()` again to re-arm +# the sticky disable. +``` + +#### Checking the disable state from integrations + +If you're writing an integration that performs telemetry setup as a side effect (e.g. provisioning a third-party exporter), consult the public read-only `is_user_disabled` property and early-return when it's set: + +```python +from agent_framework.observability import OBSERVABILITY_SETTINGS + +if OBSERVABILITY_SETTINGS.is_user_disabled: + logger.info( + "Skipping telemetry setup because the user called disable_instrumentation()." + ) + return +``` + +This is what the built-in `FoundryChatClient.configure_azure_monitor()` and `FoundryAgent.configure_azure_monitor()` do — so calling `disable_instrumentation()` reliably prevents Azure Monitor's global providers from being installed by those helpers. + +#### What `disable_instrumentation()` does **not** do + +- It does not tear down OpenTelemetry providers, exporters, or in-flight spans that were already set up before the disable call. It only gates **future** captures by Agent Framework code paths. +- It does not stop telemetry from third-party instrumentations (e.g. `azure-monitor-opentelemetry`'s system metrics) that are wired up outside Agent Framework. Configure those separately if needed. +- It does not persist across processes. Each Python process starts with the disable flag cleared; if you always want telemetry off in a given environment, set `ENABLE_INSTRUMENTATION=false` as an environment variable in addition to (or instead of) the programmatic call. #### Environment variables for `configure_otel_providers()` @@ -202,7 +304,8 @@ The `configure_otel_providers()` function automatically reads **standard OpenTel > **Note**: These are standard OpenTelemetry environment variables. See the [OpenTelemetry spec](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) for more details. #### Logging -Use standard Python logging configuration to align logs with telemetry output. + +Use standard Python logging configuration to align logs with telemetry output: ```python import logging @@ -212,15 +315,14 @@ logging.basicConfig( datefmt="%Y-%m-%d %H:%M:%S", ) ``` -You can control at what level logging happens and thus what logs get exported, you can do this, by adding this: + +To control which logs are exported, adjust the root logger level — other loggers inherit from it by default: ```python import logging -logger = logging.getLogger() -logger.setLevel(logging.NOTSET) +logging.getLogger().setLevel(logging.NOTSET) ``` -This gets the root logger and sets the level of that, automatically other loggers inherit from that one, and you will get detailed logs in your telemetry. ## Samples @@ -228,36 +330,35 @@ This folder contains different samples demonstrating how to use telemetry in var | Sample | Description | |--------|-------------| -| [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | **Recommended starting point**: Shows how to create custom exporters with specific configuration and pass them to `configure_otel_providers()`. Useful for advanced scenarios. | -| [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | Shows how to setup telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). | -| [agent_observability.py](./agent_observability.py) | Shows telemetry collection for an agentic application with tool calls using environment variables. | -| [foundry_tracing.py](./foundry_tracing.py) | Shows Azure Monitor integration with Foundry for any chat client. | -| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: Shows manual setup of exporters and providers with console output. Useful for understanding how observability works under the hood. | -| [advanced_zero_code.py](./advanced_zero_code.py) | Advanced: Shows zero-code telemetry setup using the `opentelemetry-enable_instrumentation` CLI tool. | -| [workflow_observability.py](./workflow_observability.py) | Shows telemetry collection for a workflow with multiple executors and message passing. | +| [configure_otel_providers_with_env_var.py](./configure_otel_providers_with_env_var.py) | **Recommended starting point**: configure telemetry using standard OpenTelemetry environment variables (`OTEL_EXPORTER_OTLP_*`). | +| [configure_otel_providers_with_parameters.py](./configure_otel_providers_with_parameters.py) | Create custom exporters with specific configuration and pass them to `configure_otel_providers()`. | +| [agent_observability.py](./agent_observability.py) | Telemetry collection for an agentic application with tool calls. | +| [foundry_tracing.py](./foundry_tracing.py) | Azure Monitor integration with Microsoft Foundry. | +| [workflow_observability.py](./workflow_observability.py) | Telemetry collection for a workflow with multiple executors and message passing. | +| [advanced_manual_setup_console_output.py](./advanced_manual_setup_console_output.py) | Advanced: manual setup of exporters and providers with console output — useful for understanding how observability works under the hood. | +| [advanced_zero_code.py](./advanced_zero_code.py) | Advanced: zero-code provider/exporter setup using the `opentelemetry-instrument` CLI wrapper. | ### Running the samples -1. Open a terminal and navigate to this folder: `python/samples/02-agents/observability/`. This is necessary for the `.env` file to be read correctly. -2. Create a `.env` file if one doesn't already exist in this folder. Please refer to the [example file](./.env.example). - > **Note**: You can start with just `ENABLE_INSTRUMENTATION=true` and add `OTEL_EXPORTER_OTLP_ENDPOINT` or other configuration as needed. If no exporters are configured, you can set `ENABLE_CONSOLE_EXPORTERS=true` for console output. -3. Choose one environment-loading approach: - - **A. Sample-managed loading (current samples):** run from this folder so the sample's `load_dotenv()` call can find `.env`. - - **B. Shell/IDE-managed environment:** set/export environment variables directly, or use an IDE run configuration that injects env vars / `.env`. - - **C. Explicit env file in code:** pass `env_file_path` to APIs like `configure_otel_providers(env_file_path=".env")` (or your own settings loader path). - - **D. CLI-managed env file:** run with `uv` and pass the file explicitly, for example: - `uv run --env-file=.env python configure_otel_providers_with_env_var.py` -4. Activate your python virtual environment, then run a sample (for example `python configure_otel_providers_with_env_var.py`). +1. Open a terminal in this folder (`python/samples/02-agents/observability/`) so that `.env` is found. +2. Create a `.env` file if you don't already have one. See [.env.example](./.env.example). + > Instrumentation is on by default. Set `OTEL_EXPORTER_OTLP_ENDPOINT` (or other configuration) as needed. With no exporters configured, set `ENABLE_CONSOLE_EXPORTERS=true` for console output. +3. Pick an environment-loading approach: + - **A. Sample-managed:** run from this folder so the sample's `load_dotenv()` call can find `.env`. + - **B. Shell/IDE-managed:** export environment variables, or use an IDE run configuration that injects them. + - **C. Explicit env file in code:** pass `env_file_path` to APIs like `configure_otel_providers(env_file_path=".env")`. + - **D. CLI-managed:** run with `uv` and pass the file explicitly, e.g. `uv run --env-file=.env python configure_otel_providers_with_env_var.py`. +4. Activate your virtual environment, then run a sample (e.g. `python configure_otel_providers_with_env_var.py`). -> If you do manual provider setup (e.g., Azure Monitor), call `enable_instrumentation()` to turn on Agent Framework telemetry code paths; if you want Agent Framework to configure exporters/providers for you, call `configure_otel_providers(...)`. +> If you set up providers manually (e.g. Azure Monitor), Agent Framework instrumentation is still on by default. Call `enable_sensitive_telemetry()` if you also want to capture sensitive data. To have Agent Framework configure exporters and providers for you, call `configure_otel_providers(...)`. -> Each sample will print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard. +> Each sample prints its Operation/Trace ID, which you can use to filter logs and traces in Application Insights or the Aspire Dashboard. # Appendix ## Azure Monitor Queries -When you are in Azure Monitor and want to have a overall view of the span, use this query in the logs section: +For an overall view of a span in Azure Monitor, run this query in the Logs section: ```kusto dependencies @@ -280,7 +381,8 @@ dependencies ``` ### Grafana dashboards with Application Insights data -Besides the Application Insights native UI, you can also use Grafana to visualize the telemetry data in Application Insights. There are two tailored dashboards for you to get started quickly: + +In addition to the native Application Insights UI, you can use Grafana to visualize the same telemetry data. Two tailored dashboards are available to get you started: #### Agent Overview dashboard Open dashboard in Azure portal: @@ -292,117 +394,27 @@ Open dashboard in Azure portal: ## Migration Guide -We've done a major update to the observability API in Agent Framework Python SDK. The new API simplifies configuration by relying more on standard OpenTelemetry environment variables and have split the instrumentation from the configuration. +Instrumentation is now **enabled by default** (you no longer have to opt in by calling `enable_instrumentation()` at startup), and the way you opt in to capturing sensitive payloads has its own dedicated function. -If you're updating from a previous version of the Agent Framework, here are the key changes to the observability API: - -### Environment Variables - -| Old Variable | New Variable | Notes | -|-------------|--------------|-------| -| `OTLP_ENDPOINT` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Standard OpenTelemetry env var | -| `APPLICATIONINSIGHTS_CONNECTION_STRING` | N/A | Use `configure_azure_monitor()` | -| N/A | `ENABLE_CONSOLE_EXPORTERS` | New opt-in flag for console output | - -### OTLP Configuration - -**Before (Deprecated):** -``` -from agent_framework.observability import setup_observability -# Via parameter -setup_observability(otlp_endpoint="http://localhost:4317") - -# Via environment variable -# OTLP_ENDPOINT=http://localhost:4317 -setup_observability() -``` - -**After (Current):** -```python -from agent_framework.observability import configure_otel_providers -# Via standard OTEL environment variable (recommended) -# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 -configure_otel_providers() - -# Or via custom exporters -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter -from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter - -configure_otel_providers(exporters=[ - OTLPSpanExporter(endpoint="http://localhost:4317"), - OTLPLogExporter(endpoint="http://localhost:4317"), - OTLPMetricExporter(endpoint="http://localhost:4317"), -]) -``` - -### Azure Monitor Configuration - -**Before (Deprecated):** -``` -from agent_framework.observability import setup_observability - -setup_observability( - applicationinsights_connection_string="InstrumentationKey=...", - applicationinsights_live_metrics=True, -) -``` - -**After (Current):** +If your code previously did: ```python -from agent_framework.foundry import FoundryChatClient -from agent_framework.observability import create_resource, enable_instrumentation -from azure.identity import AzureCliCredential -from azure.monitor.opentelemetry import configure_azure_monitor +from agent_framework.observability import enable_instrumentation -async def main(): - # For Microsoft Foundry projects - client = FoundryChatClient( - project_endpoint="https://your-project.services.ai.azure.com", - model="gpt-4o", - credential=AzureCliCredential(), - ) - await client.configure_azure_monitor(enable_live_metrics=True) - - # For non-Azure AI projects - configure_azure_monitor( - connection_string="InstrumentationKey=...", - resource=create_resource(), - enable_live_metrics=True, - ) - enable_instrumentation() +enable_instrumentation(enable_sensitive_data=True) ``` -### Console Output +replace it with: -**Before (Deprecated):** -``` -from agent_framework.observability import setup_observability - -# Console was used as automatic fallback -setup_observability() # Would output to console if no exporters configured -``` - -**After (Current):** ```python -from agent_framework.observability import configure_otel_providers +from agent_framework.observability import enable_sensitive_telemetry -# Console exporters are now opt-in -# ENABLE_CONSOLE_EXPORTERS=true -configure_otel_providers() - -# Or programmatically -configure_otel_providers(enable_console_exporters=True) +enable_sensitive_telemetry() ``` -### Benefits of New API +`enable_sensitive_telemetry()` ensures that instrumentation is on and turns sensitive-event capture on in one call. `enable_instrumentation()` still exists for the rare case where you want to programmatically force instrumentation on without enabling sensitive data (e.g. to override `ENABLE_INSTRUMENTATION=false`), and it now also accepts `force=True` to clear a previous `disable_instrumentation()` — see [Disabling instrumentation](#disabling-instrumentation). -1. **Standards Compliant**: Uses standard OpenTelemetry environment variables -2. **Simpler**: Less configuration needed, more relies on environment -3. **Flexible**: Easy to add custom exporters alongside environment-based ones -4. **Cleaner Separation**: Azure Monitor setup is in Azure-specific client -5. **Better Compatibility**: Works with any OTEL-compatible tool (Jaeger, Zipkin, Prometheus, etc.) +> **Note**: Sensitive data includes prompts, responses, and tool arguments. Only enable it in development or test environments — it may expose user or system secrets in production. ## Aspire Dashboard @@ -437,7 +449,7 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 Or set it as an environment variable when running your samples: ```bash -ENABLE_INSTRUMENTATION=true OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python configure_otel_providers_with_env_var.py +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python configure_otel_providers_with_env_var.py ``` ### Viewing telemetry data diff --git a/python/samples/02-agents/observability/__init__.py b/python/samples/02-agents/observability/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/samples/02-agents/observability/advanced_manual_setup_console_output.py b/python/samples/02-agents/observability/advanced_manual_setup_console_output.py index 722cbf445a..8ae3b17dfa 100644 --- a/python/samples/02-agents/observability/advanced_manual_setup_console_output.py +++ b/python/samples/02-agents/observability/advanced_manual_setup_console_output.py @@ -7,7 +7,7 @@ from typing import Annotated from agent_framework import Message, tool from agent_framework.foundry import FoundryChatClient -from agent_framework.observability import enable_instrumentation +from agent_framework.observability import enable_sensitive_telemetry from azure.identity import AzureCliCredential from dotenv import load_dotenv from opentelemetry._logs import set_logger_provider @@ -135,7 +135,8 @@ async def main(): setup_logging() setup_tracing() setup_metrics() - enable_instrumentation() + # Instrumentation is enabled by default; call this to also capture sensitive data. + enable_sensitive_telemetry() await run_chat_client() diff --git a/python/samples/02-agents/observability/advanced_zero_code.py b/python/samples/02-agents/observability/advanced_zero_code.py index dffd26a0fc..cd361b1ef2 100644 --- a/python/samples/02-agents/observability/advanced_zero_code.py +++ b/python/samples/02-agents/observability/advanced_zero_code.py @@ -19,13 +19,20 @@ if TYPE_CHECKING: """ This sample shows how you can configure observability of an application with zero code changes. -It relies on the OpenTelemetry auto-instrumentation capabilities, and the observability setup -is done via environment variables. -Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool, -when using `uv` there are some additional steps, so follow the instructions carefully. +Agent Framework is natively instrumented with OpenTelemetry, so no auto-instrumentation of the +framework itself is required. Running the `opentelemetry-instrument` CLI wrapper simply configures +the global tracer/meter providers and exporters from environment variables (or CLI flags) at +process startup, so the application code does not need to set them up explicitly. The native +spans/metrics emitted by Agent Framework are then picked up by that globally configured pipeline. -And setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update the endpoint below). +See: https://opentelemetry.io/docs/zero-code/python/ + +Install the OpenTelemetry CLI tool following the guidance above (when using `uv` there are some +additional steps, so follow the instructions carefully). + +Then setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update +the endpoint below). Then you can run: ```bash diff --git a/python/samples/03-workflows/observability/executor_io_observation.py b/python/samples/03-workflows/observability/executor_io_observation.py index 3129fcf158..4637f8c975 100644 --- a/python/samples/03-workflows/observability/executor_io_observation.py +++ b/python/samples/03-workflows/observability/executor_io_observation.py @@ -22,9 +22,6 @@ What this example shows: - executor_completed events (type='executor_completed') contain the messages sent via ctx.send_message() in event.data - How to generically observe all executor I/O through workflow streaming events -This approach allows you to enable_instrumentation any workflow for observability without -changing the executor implementations. - Prerequisites: - No external services required. """ diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example index f53b64c8c5..bf9bff7405 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/.env.example @@ -1,4 +1,3 @@ FOUNDRY_PROJECT_ENDPOINT="..." AZURE_AI_MODEL_DEPLOYMENT_NAME="..." -ENABLE_INSTRUMENTATION=true ENABLE_SENSITIVE_DATA=true \ No newline at end of file diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md index 9f08baa168..9b592f2079 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/README.md @@ -16,7 +16,7 @@ The agent is hosted using the [Agent Framework](https://github.com/microsoft/age ### Instrumentation -Agent Framework is [**natively instrumented**](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) to capture diagnostics and telemetry for agent execution, but it's turned off by default. This sample demonstrates how to enable instrumentation via environment variables in `agent.manifest.yaml` and `agent.yaml`. The relevant environment variables are `ENABLE_INSTRUMENTATION` and `ENABLE_SENSITIVE_DATA`, which can be set to `true` to enable diagnostics and capture sensitive events respectively. +Agent Framework is [**natively instrumented**](https://learn.microsoft.com/en-us/agent-framework/agents/observability?pivots=programming-language-python) to capture diagnostics and telemetry for agent execution. Instrumentation is enabled by default. To also capture sensitive event payloads (prompts, tool arguments, etc.) set `ENABLE_SENSITIVE_DATA=true`. This sample demonstrates how to manage these settings via environment variables in `agent.manifest.yaml` and `agent.yaml`. Foundry Hosted Agent has built-in observability thus you don't need to set up exporters manually to capture telemetry from your code. The traces, metrics, and logs generated by the agent are automatically collected and made available through Foundry's observability stack via Azure Monitor/Application Insights. The `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable is injected when the agent is deployed to Foundry, however it is still required to be set in your environment if you want to run the agent host locally and have telemetry sent to Application Insights from your local environment. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml index b96c34ad96..845f462952 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.manifest.yaml @@ -17,8 +17,6 @@ template: environment_variables: - name: AZURE_AI_MODEL_DEPLOYMENT_NAME value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" - - name: ENABLE_INSTRUMENTATION - value: true - name: ENABLE_SENSITIVE_DATA value: true resources: diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml index 216dd415d6..f0e651136e 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/07_observability/agent.yaml @@ -5,12 +5,10 @@ protocols: - protocol: responses version: 1.0.0 resources: - cpu: '0.25' - memory: '0.5Gi' + cpu: "0.25" + memory: "0.5Gi" environment_variables: - name: AZURE_AI_MODEL_DEPLOYMENT_NAME value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} - - name: ENABLE_INSTRUMENTATION - value: true - name: ENABLE_SENSITIVE_DATA - value: true \ No newline at end of file + value: true diff --git a/python/samples/README.md b/python/samples/README.md index 6017d578f6..2ee739d860 100644 --- a/python/samples/README.md +++ b/python/samples/README.md @@ -90,7 +90,7 @@ Example values below are illustrative. For entries not backed by a single public column names the closest public surface, helper, or package-level initialization point that reads the variable. -| package | class | env var | example value | +| package | class/module | env var | example value | | --- | --- | --- | --- | | `agent-framework-anthropic` | `AnthropicClient` | `ANTHROPIC_API_KEY` | `sk-ant-api03-...` | | `agent-framework-anthropic` | `AnthropicClient` | `ANTHROPIC_CHAT_MODEL` | `claude-sonnet-4-5-20250929` | @@ -117,21 +117,21 @@ variable. | `agent-framework-copilotstudio` | `CopilotStudioAgent` | `COPILOTSTUDIOAGENT__SCHEMANAME` | `cr123_agentname` | | `agent-framework-copilotstudio` | `CopilotStudioAgent` | `COPILOTSTUDIOAGENT__TENANTID` | `11111111-1111-1111-1111-111111111111` | | `agent-framework-copilotstudio` | `CopilotStudioAgent` | `COPILOTSTUDIOAGENT__AGENTAPPID` | `22222222-2222-2222-2222-222222222222` | -| `agent-framework-core` | `enable_instrumentation()` | `ENABLE_INSTRUMENTATION` | `true` | -| `agent-framework-core` | `enable_instrumentation()` | `ENABLE_SENSITIVE_DATA` | `false` | -| `agent-framework-core` | `enable_instrumentation()` | `ENABLE_CONSOLE_EXPORTERS` | `true` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `http://localhost:4318/v1/traces` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `http://localhost:4318/v1/metrics` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | `http://localhost:4318/v1/logs` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_HEADERS` | `api-key=demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | `api-key=trace-demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | `api-key=metric-demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | `api-key=log-demo` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_SERVICE_NAME` | `sample-agent` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_SERVICE_VERSION` | `1.0.0` | -| `agent-framework-core` | `enable_instrumentation()` | `OTEL_RESOURCE_ATTRIBUTES` | `deployment.environment=dev,service.namespace=agent-framework` | +| `agent-framework-core` | `observability` | `ENABLE_INSTRUMENTATION` | `true` | +| `agent-framework-core` | `observability` | `ENABLE_SENSITIVE_DATA` | `false` | +| `agent-framework-core` | `observability` | `ENABLE_CONSOLE_EXPORTERS` | `true` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `http://localhost:4318/v1/traces` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `http://localhost:4318/v1/metrics` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | `http://localhost:4318/v1/logs` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_HEADERS` | `api-key=demo` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | `api-key=trace-demo` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | `api-key=metric-demo` | +| `agent-framework-core` | `observability` | `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | `api-key=log-demo` | +| `agent-framework-core` | `observability` | `OTEL_SERVICE_NAME` | `sample-agent` | +| `agent-framework-core` | `observability` | `OTEL_SERVICE_VERSION` | `1.0.0` | +| `agent-framework-core` | `observability` | `OTEL_RESOURCE_ATTRIBUTES` | `deployment.environment=dev,service.namespace=agent-framework` | | `agent-framework-devui` | `DevUI server` | `DEVUI_AUTH_TOKEN` | `my-devui-token` | | `agent-framework-foundry` | `FoundryChatClient` | `FOUNDRY_PROJECT_ENDPOINT` | `https://my-project.services.ai.azure.com/api/projects/my-project` | | `agent-framework-foundry` | `FoundryChatClient` | `FOUNDRY_MODEL` | `gpt-4o` | diff --git a/python/uv.lock b/python/uv.lock index 1b932afa8c..b6436f951b 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -603,7 +603,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0b2,<=1.0.0b2" }, ] [[package]] From d74d26c917a98d23f7e537429885ca51724cf74a Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Wed, 20 May 2026 05:00:38 -0700 Subject: [PATCH 15/22] Python: Show more authentication methods in Foundry Toolbox MCP (#5719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Show more authentication methods in Foundry Toolbox MCP * Remove hardcoded toolbox version num * Add Foundry MCP OAuth consent handling * Use message instead of the dedicated item type * Go back to using OAuthConsentRequestOutputItem * WIP: sample testing * Update error code * Address review on Foundry Toolbox MCP samples Reviewed feedback addressed: - Drop the branch-pinned `git+https://...@feature/...` entries from `04_foundry_toolbox/requirements.txt`; restore the simple comment + `mcp` runtime dep. The git pins were only useful while iterating on the PR and shouldn't ship. (eavanvalkenburg) - Fix the `/toolsets/` typo in both `04_foundry_toolbox/README.md` and `06_files/README.md`. Verified empirically against the research_toolbox in the test workspace: the toolbox MCP gateway lives at `/toolboxes/{name}/mcp?api-version=v1` and requires the `Foundry-Features: Toolboxes=V1Preview` header. `/toolsets/{name}/mcp` returns 403 with `preview_feature_required: Toolsets=V1Preview` (a different opt-in feature). - Wrap `httpx.AsyncClient(...)` in `async with ... as http_client:` in both samples so the connection pool is cleaned up. (Copilot reviewer) - Make the `TOOLBOX_NAME` env var consistent in both samples. Previously the tool name silently fell back to `"toolbox"` when `TOOLBOX_NAME` was unset, but `resolve_toolbox_endpoint()` still required `TOOLBOX_NAME` and would raise `KeyError`. The samples now resolve the endpoint once and derive the tool name from the resolved URL when `TOOLBOX_NAME` isn't set, so the local tool name always matches the upstream toolbox identity regardless of which env var the user set. (Copilot reviewer) - Rename `_responses.is_consent_error` to `consent_url_from_error`: the helper returns `str | None` (the consent URL), not a bool, so the new name matches behavior. Update the test class accordingly. (eavanvalkenburg) - Tighten `_handle_inner_agent`'s lazy-entry catch from `Exception` to `AgentFrameworkException`, the type the MCP layer actually wraps consent errors in via `MCPStreamableHTTPTool.__aenter__` → `ToolExecutionException(inner_exception=mcp_error)`. Network failures, cancellations, and other non-framework exceptions now propagate normally instead of being briefly caught and re-raised. The test helper `_make_consent_error` is updated to use `ToolExecutionException` so it matches the real-world wrapping. (eavanvalkenburg) - Clarify the `github_pat` description in `agent.manifest.yaml` to note it's only needed when the PAT-based connection (`github-mcp-pat-conn`) is chosen; users selecting the OAuth2 connection (`github-mcp-oauth-conn`) can leave it empty. (Copilot reviewer) Validation: ran both samples end-to-end against a real Foundry toolbox (`research_toolbox`) -- the samples connect successfully and the agent lists the toolbox's MCP tools (`api_specs___fetch_azure_rest_api_docs`, etc.). `uv run poe test -P foundry_hosting` passes (119 tests), pyright + mypy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: fix broken Foundry samples link in 04_foundry_toolbox README The previous URL pointed to an old location of the toolbox supported-scenarios doc; the doc moved to /samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md and the old /samples/python/toolbox/azd path now 404s. Caught by the markdown-link-check CI step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Eduard van Valkenburg Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../_responses.py | 175 ++++++++++++++--- .../foundry_hosting/tests/test_responses.py | 185 ++++++++++++++++++ .../responses/04_foundry_toolbox/Dockerfile | 2 + .../responses/04_foundry_toolbox/README.md | 20 +- .../04_foundry_toolbox/agent.manifest.yaml | 94 ++++++++- .../responses/04_foundry_toolbox/main.py | 79 ++++---- .../04_foundry_toolbox/requirements.txt | 6 +- .../responses/06_files/README.md | 11 +- .../responses/06_files/main.py | 92 +++++---- 9 files changed, 537 insertions(+), 127 deletions(-) diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py index c34c65538c..49a461f9b1 100644 --- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py +++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py @@ -12,6 +12,7 @@ import threading from collections.abc import AsyncIterable, AsyncIterator, Generator, Mapping, Sequence from contextlib import suppress from pathlib import Path +from contextlib import AbstractAsyncContextManager, AsyncExitStack, suppress from typing import Protocol, cast from agent_framework import ( @@ -25,12 +26,14 @@ from agent_framework import ( SupportsAgentRun, WorkflowAgent, ) +from agent_framework.exceptions import AgentFrameworkException from azure.ai.agentserver.responses import ( ResponseContext, ResponseEventStream, ResponseProviderProtocol, ResponsesServerOptions, ) +from azure.ai.agentserver.responses._id_generator import IdGenerator from azure.ai.agentserver.responses.hosting import ResponsesAgentServerHost from azure.ai.agentserver.responses.models import ( ApplyPatchToolCallItemParam, @@ -108,11 +111,13 @@ from azure.ai.agentserver.responses.streaming._builders import ( ReasoningSummaryPartBuilder, TextContentBuilder, ) +from mcp import McpError from typing_extensions import Any logger = logging.getLogger(__name__) +# region Approval Storage class ApprovalStorage(Protocol): """Storage for saving function approval requests.""" @@ -247,6 +252,39 @@ def _checkpoint_storage_for_context(root: str, context_id: str) -> FileCheckpoin return FileCheckpointStorage(storage_path) +# endregion Approval Storage + +# Foundry Toolbox Auth integration +# Consent-URL error code returned by the Foundry MCP gateway when calling `/list` +CONSENT_ERROR_CODE = -32007 + + +def consent_url_from_error(exc: BaseException) -> str | None: + """Return the consent URL when ``exc`` wraps a Foundry MCP gateway consent error. + + The Agent Framework MCP layer surfaces gateway consent failures by wrapping the underlying + ``McpError`` inside an :class:`AgentFrameworkException` (typically a ``ToolExecutionException`` + raised from ``MCPStreamableHTTPTool.__aenter__``). This helper inspects ``exc.args`` for a + wrapped ``McpError`` whose ``error.code`` is :data:`CONSENT_ERROR_CODE`; when found, the + consent link the gateway returned in ``error.message`` is returned. Returns ``None`` for + anything else, so callers can do ``if (url := consent_url_from_error(ex)) is None: raise``. + + Args: + exc: The exception to inspect. + + Returns: + The consent URL if ``exc`` wraps a consent ``McpError``, otherwise ``None``. + """ + inner_exception = next((arg for arg in exc.args if isinstance(arg, McpError)), None) + if inner_exception is not None and inner_exception.error.code == CONSENT_ERROR_CODE: + return inner_exception.error.message + return None + + +# endregion Foundry Toolbox Auth integration + + +# region ResponsesHostServer class ResponsesHostServer(ResponsesAgentServerHost): """A responses server host for an agent.""" @@ -315,8 +353,43 @@ class ResponsesHostServer(ResponsesAgentServerHost): if self.config.is_hosted else InMemoryFunctionApprovalStorage() ) + # Lazy agent lifecycle: the agent (and any MCP tools it owns) is entered on + # the first request rather than at server startup, so that authentication + # failures during MCP connect can be surfaced to the client as an + # `oauth_consent_request` stream event instead of crashing the server. + self._agent_stack: AsyncExitStack | None = None + self._agent_init_lock = asyncio.Lock() + self.shutdown_handler(self._cleanup_agent) # pyright: ignore[reportUnknownMemberType] self.response_handler(self._handle_response) # pyright: ignore[reportUnknownMemberType] + async def _ensure_agent_ready(self) -> None: + """Lazily enter the agent's async context exactly once. + + On failure the partial exit stack is closed and ``_agent_stack`` is left + as ``None`` so a subsequent request (e.g. after the user completes OAuth + consent) can retry the connection. + """ + if self._agent_stack is not None: + return + async with self._agent_init_lock: + if self._agent_stack is not None: + return + stack = AsyncExitStack() + try: + if isinstance(self._agent, AbstractAsyncContextManager): + await stack.enter_async_context(self._agent) + except BaseException: + await stack.aclose() + raise + self._agent_stack = stack + + async def _cleanup_agent(self) -> None: + """Close the agent's async context. Registered as the server shutdown handler.""" + stack = self._agent_stack + if stack is not None: + self._agent_stack = None + await stack.aclose() + async def _handle_response( self, request: CreateResponse, @@ -359,45 +432,76 @@ class ResponsesHostServer(ResponsesAgentServerHost): else: run_kwargs["options"] = chat_options - if not is_streaming_request: - # Run the agent in non-streaming mode - response = await self._agent.run(stream=False, **run_kwargs) # type: ignore[reportUnknownMemberType] - - for message in response.messages: - for content in message.contents: - async for item in _to_outputs( - response_event_stream, - content, - approval_storage=self._approval_storage, - ): - yield item - + # Lazy-enter the agent (and any MCP tools it owns). The MCP client wraps gateway + # consent failures (and other connection-time errors) in AgentFrameworkException; if + # one of those is a consent error we surface the consent link to the client through + # the already-opened response stream instead of crashing the request. Other exception + # types propagate normally so the host can handle / log them. + try: + await self._ensure_agent_ready() + except AgentFrameworkException as ex: + consent_url = consent_url_from_error(ex) + if consent_url is None: + raise + logger.warning("OAuth consent required for Foundry MCP gateway.") + oauth_item = OAuthConsentRequestOutputItem( + id=IdGenerator.new_id("oacr"), + consent_link=consent_url, + server_label="Foundry Toolbox", + ) + builder = response_event_stream.add_output_item(oauth_item.id) + yield builder.emit_added(oauth_item) + yield builder.emit_done(oauth_item) yield response_event_stream.emit_completed() return # Track the current active output item builder for streaming; # lazily created on matching content, closed when a different type arrives. - tracker = _OutputItemTracker(response_event_stream) + tracker: _OutputItemTracker | None = _OutputItemTracker(response_event_stream) if is_streaming_request else None - # Run the agent in streaming mode - async for update in self._agent.run(stream=True, **run_kwargs): # type: ignore[reportUnknownMemberType] - for content in update.contents: - for event in tracker.handle(content): + try: + if not is_streaming_request: + # Run the agent in non-streaming mode + response = await self._agent.run(stream=False, **run_kwargs) # type: ignore[reportUnknownMemberType] + + for message in response.messages: + for content in message.contents: + async for item in _to_outputs( + response_event_stream, + content, + approval_storage=self._approval_storage, + ): + yield item + yield response_event_stream.emit_completed() + else: + if tracker is None: # pragma: no cover - defensive, set above + raise RuntimeError("Streaming tracker was not initialized.") + # Run the agent in streaming mode + async for update in self._agent.run(stream=True, **run_kwargs): # type: ignore[reportUnknownMemberType] + for content in update.contents: + for event in tracker.handle(content): + yield event + if tracker.needs_async: + async for item in _to_outputs( + response_event_stream, + content, + approval_storage=self._approval_storage, + ): + yield item + tracker.needs_async = False + + # Close any remaining active builder + for event in tracker.close(): yield event - if tracker.needs_async: - async for item in _to_outputs( - response_event_stream, - content, - approval_storage=self._approval_storage, - ): - yield item - tracker.needs_async = False - - # Close any remaining active builder - for event in tracker.close(): - yield event - - yield response_event_stream.emit_completed() + yield response_event_stream.emit_completed() + except Exception: + # Drain any in-progress streaming builder before emitting consent + # so the resulting stream stays well-formed. + if tracker is not None: + for event in tracker.close(): + yield event + yield response_event_stream.emit_completed() + raise async def _handle_inner_workflow( self, @@ -429,6 +533,11 @@ class ResponsesHostServer(ResponsesAgentServerHost): if not isinstance(self._agent, WorkflowAgent): raise RuntimeError("Agent is not a workflow agent.") + # Workflow agents are not async context managers in any built-in path, + # but call _ensure_agent_ready for symmetry with the regular path so + # any future async resources owned by the workflow are entered here. + await self._ensure_agent_ready() + # Determine the latest checkpoint (if any) so we can resume the # workflow's prior state for this turn. The directory is keyed by # the inbound context id (conversation_id when set, otherwise @@ -551,6 +660,8 @@ class ResponsesHostServer(ResponsesAgentServerHost): await checkpoint_storage.delete(checkpoint.checkpoint_id) +# endregion ResponsesHostServer + # region Active Builder State diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py index e4d545d6d7..46a3d7f8ef 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -27,14 +27,18 @@ from agent_framework import ( ResponseStream, ) from azure.ai.agentserver.responses import InMemoryResponseProvider +from mcp import McpError +from mcp.types import ErrorData from typing_extensions import Any from agent_framework_foundry_hosting import ResponsesHostServer from agent_framework_foundry_hosting._responses import ( + CONSENT_ERROR_CODE, FileBasedFunctionApprovalStorage, # pyright: ignore[reportPrivateUsage] InMemoryFunctionApprovalStorage, # pyright: ignore[reportPrivateUsage] _item_to_message, # pyright: ignore[reportPrivateUsage] _output_item_to_message, # pyright: ignore[reportPrivateUsage] + consent_url_from_error, ) @@ -2888,6 +2892,187 @@ class TestCheckpointContextPathValidation: f"before={before} after={after}" ) assert list(root.iterdir()) == [], f"Checkpoint directory created inside root for {context_field}={bad_id!r}" +# region Agent lifecycle (lazy entry & OAuth consent surfacing) + + +def _make_consent_error(url: str = "https://consent.example.com/auth") -> Exception: + """Build an exception wrapping a Foundry MCP gateway consent error. + + Mirrors the real-world wrapping produced by ``MCPStreamableHTTPTool.__aenter__``, + which catches connection-time ``McpError``s and re-raises them as a + ``ToolExecutionException`` (an ``AgentFrameworkException`` subclass) with the + original error attached via ``inner_exception``. ``consent_url_from_error`` + then finds the wrapped ``McpError`` in ``exc.args``. + """ + from agent_framework.exceptions import ToolExecutionException + + inner = McpError(ErrorData(code=CONSENT_ERROR_CODE, message=url)) + return ToolExecutionException("MCP consent required", inner_exception=inner) + + +class TestConsentUrlFromError: + def test_returns_consent_url_when_inner_arg_is_consent_mcp_error(self) -> None: + exc = _make_consent_error("https://example.com/consent") + assert consent_url_from_error(exc) == "https://example.com/consent" + + def test_returns_none_when_no_mcp_error_in_args(self) -> None: + assert consent_url_from_error(Exception("boom")) is None + + def test_returns_none_when_mcp_error_has_different_code(self) -> None: + inner = McpError(ErrorData(code=-32000, message="some other error")) + exc = Exception("wrapped", inner) + assert consent_url_from_error(exc) is None + + def test_returns_none_for_bare_mcp_error_without_wrapping(self) -> None: + # `args` of a bare McpError holds the message string, not an McpError + # instance, so it does not match the wrapping pattern produced by the + # MCP client when it bubbles consent errors up. + bare = McpError(ErrorData(code=CONSENT_ERROR_CODE, message="https://x")) + assert consent_url_from_error(bare) is None + + +class TestAgentLifecycle: + async def test_agent_entered_lazily_on_first_request(self) -> None: + agent = _make_agent( + response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])]) + ) + server = _make_server(agent) + # Construction must not enter the agent. + assert agent.__aenter__.await_count == 0 + + await _post(server, input_text="hello", stream=False) + assert agent.__aenter__.await_count == 1 + + async def test_agent_entered_only_once_across_requests(self) -> None: + agent = _make_agent( + response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])]) + ) + server = _make_server(agent) + + await _post(server, input_text="first", stream=False) + await _post(server, input_text="second", stream=False) + await _post(server, input_text="third", stream=False) + assert agent.__aenter__.await_count == 1 + + async def test_cleanup_exits_agent_and_allows_reentry(self) -> None: + agent = _make_agent( + response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])]) + ) + server = _make_server(agent) + + await _post(server, input_text="hello", stream=False) + assert agent.__aenter__.await_count == 1 + assert agent.__aexit__.await_count == 0 + + await server._cleanup_agent() # pyright: ignore[reportPrivateUsage] + assert agent.__aexit__.await_count == 1 + + # Cleanup is idempotent. + await server._cleanup_agent() # pyright: ignore[reportPrivateUsage] + assert agent.__aexit__.await_count == 1 + + # After cleanup, a follow-up request re-enters the agent. + await _post(server, input_text="again", stream=False) + assert agent.__aenter__.await_count == 2 + + async def test_failed_entry_does_not_cache_stack(self) -> None: + agent = _make_agent( + response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])]) + ) + agent.__aenter__.side_effect = [_make_consent_error(), None] + server = _make_server(agent) + + await _post(server, input_text="first", stream=False) + # Failed entry must leave the stack empty so the next request retries. + await _post(server, input_text="second", stream=False) + assert agent.__aenter__.await_count == 2 + + +class TestOAuthConsentSurfacing: + async def test_non_streaming_consent_error_emits_oauth_output_item(self) -> None: + agent = _make_agent( + response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])]) + ) + agent.__aenter__.side_effect = _make_consent_error("https://consent.example.com/auth") + server = _make_server(agent) + + resp = await _post(server, input_text="hello", stream=False) + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "completed" + + oauth_items = [it for it in body["output"] if it["type"] == "oauth_consent_request"] + assert len(oauth_items) == 1 + assert oauth_items[0]["consent_link"] == "https://consent.example.com/auth" + assert oauth_items[0]["server_label"] == "Foundry Toolbox" + + # The agent must not be run when entry fails. + agent.run.assert_not_called() + + async def test_streaming_consent_error_emits_oauth_output_item(self) -> None: + agent = _make_agent(stream_updates=[AgentResponseUpdate(contents=[Content.from_text("hi")], role="assistant")]) + agent.__aenter__.side_effect = _make_consent_error("https://consent.example.com/auth") + server = _make_server(agent) + + resp = await _post(server, input_text="hello", stream=True) + assert resp.status_code == 200 + events = _parse_sse_events(resp.text) + types = _sse_event_types(events) + + assert types[0] == "response.created" + assert types[1] == "response.in_progress" + assert types[-1] == "response.completed" + + added = [e for e in events if e["event"] == "response.output_item.added"] + oauth_added = [e for e in added if e["data"]["item"]["type"] == "oauth_consent_request"] + assert len(oauth_added) == 1 + assert oauth_added[0]["data"]["item"]["consent_link"] == "https://consent.example.com/auth" + assert oauth_added[0]["data"]["item"]["server_label"] == "Foundry Toolbox" + + done = [e for e in events if e["event"] == "response.output_item.done"] + assert any(e["data"]["item"]["type"] == "oauth_consent_request" for e in done) + + agent.run.assert_not_called() + + async def test_non_consent_error_during_entry_propagates(self) -> None: + agent = _make_agent( + response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])]) + ) + agent.__aenter__.side_effect = RuntimeError("boom") + server = _make_server(agent) + + resp = await _post(server, input_text="hello", stream=False) + # Non-consent errors are not swallowed: the response is marked failed + # and no `oauth_consent_request` item is emitted. + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "failed" + assert not any(it["type"] == "oauth_consent_request" for it in body.get("output", [])) + agent.run.assert_not_called() + + async def test_retry_after_consent_succeeds(self) -> None: + agent = _make_agent( + response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hello!")])]) + ) + agent.__aenter__.side_effect = [_make_consent_error("https://consent.example.com/auth"), None] + server = _make_server(agent) + + # First request surfaces consent; agent.run is not called. + resp1 = await _post(server, input_text="first", stream=False) + assert resp1.status_code == 200 + body1 = resp1.json() + oauth = [it for it in body1["output"] if it["type"] == "oauth_consent_request"] + assert len(oauth) == 1 + agent.run.assert_not_called() + + # After the user authenticates, the next request enters successfully. + resp2 = await _post(server, input_text="second", stream=False) + assert resp2.status_code == 200 + body2 = resp2.json() + assert body2["status"] == "completed" + assert any(it["type"] == "message" for it in body2["output"]) + assert agent.__aenter__.await_count == 2 + agent.run.assert_awaited_once() # endregion diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/Dockerfile b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/Dockerfile index eaffb94f19..6a5bde6d26 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/Dockerfile +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/Dockerfile @@ -1,5 +1,7 @@ FROM python:3.12-slim +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY . user_agent/ diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md index bd261061c6..8a26737c86 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/README.md @@ -10,6 +10,20 @@ You can also create a Foundry Toolbox in the Foundry portal. Read more about it > If you set up a project with this sample and provision the resources using `azd provision`, a Foundry Toolbox will be created with the specified tools in [`agent.manifest.yaml`](agent.manifest.yaml). +### Authentication Methods + +You can connect to MCP servers in Foundry Toolbox that use different authentication methods. This sample demonstrates the following authentication methods: + +- **No authentication**: The tool does not require any authentication. The agent can invoke the tool without providing any credentials. Sample MCP server: `https://gitmcp.io/Azure/azure-rest-api-specs` +- **Key-based authentication**: The tool requires a key to authenticate. Sample MCP server: `https://api.githubcopilot.com/mcp` (GitHub MCP server) with a Personal Access Token (PAT) for authentication. +- **OAuth2 authentication (managed)**: The tool requires OAuth2 to authenticate. Sample MCP server: `https://api.githubcopilot.com/mcp` (GitHub MCP server) with OAuth2 for authentication. +- **Agent identity authentication**: The tool requires an agent identity token to authenticate. Sample MCP server: `https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview` (Azure Language MCP server) with agent identity for authentication. +- **Entra Pass-through authentication**: The tool requires an Entra pass-through token to authenticate. Sample MCP server: Microsoft Outlook MCP server with Entra pass-through for authentication. + +> Definitions of these authentication methods can be found in the [agent.manifest.yaml](agent.manifest.yaml) file in this sample. + +There are also Non-MCP tools in the toolbox that support different authentication methods. Learn more at the [Foundry sample repository](https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/SUPPORTED_TOOLBOX_SCENARIOS.md). + ## How It Works ### Model Integration @@ -31,20 +45,20 @@ An extra environment variable must be set to point to the toolbox MCP endpoint. **Option A – Set `FOUNDRY_TOOLBOX_ENDPOINT` directly** (recommended for local development): ```bash -export FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolsets//mcp?api-version=v1" +export FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolboxes//mcp?api-version=v1" ``` Or in PowerShell: ```powershell -$env:FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolsets//mcp?api-version=v1" +$env:FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolboxes//mcp?api-version=v1" ``` **Option B – Set `TOOLBOX_NAME`** (used automatically by the Foundry hosting scaffolding after `azd provision`): The agent derives the endpoint at runtime as: ``` -{FOUNDRY_PROJECT_ENDPOINT}/toolsets/{TOOLBOX_NAME}/mcp?api-version=v1 +{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1 ``` When deployed via `azd provision`, the scaffolding injects `TOOLBOX_NAME=agent-tools` and `FOUNDRY_PROJECT_ENDPOINT` automatically from the provisioned resources declared in [`agent.manifest.yaml`](agent.manifest.yaml). diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml index c6df32950b..c8774c04f1 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/agent.manifest.yaml @@ -18,16 +18,92 @@ template: - name: AZURE_AI_MODEL_DEPLOYMENT_NAME value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" - name: TOOLBOX_NAME - value: "agent-tools" + value: "agent-tools-2" +# parameters: +# properties: +# - name: mcp_endpoint +# # `azd ai agent init -m` will prompt for this value when initializing the agent manifest +# secret: false +# description: URL of the public MCP server (e.g. https://gitmcp.io/Azure/azure-rest-api-specs) that does not require authentication +# - name: github_pat +# # `azd ai agent init -m` will prompt for this value when initializing the agent manifest. +# # Only needed when the GitHub MCP connection is configured to use the `github-mcp-pat-conn` +# # PAT-based connection below; if you use the `github-mcp-oauth-conn` OAuth2 connection +# # instead, you can leave this empty. +# secret: true +# description: GitHub Personal Access Token used to authenticate with the GitHub MCP server (only needed when using the PAT connection; press Enter if using OAuth2 instead) +# - name: language_mcp_entra_audience +# secret: false +# description: Entra ID audience for the Azure Language MCP server (e.g. https://cognitiveservices.azure.com/) +# - name: language_mcp_target_url +# secret: false +# description: URL of the Azure Language MCP server that accepts agent identity tokens (e.g. https://{foundry-resource-name}.cognitiveservices.azure.com/language/mcp?api-version=2025-11-15-preview) +# - name: outlook_mail_entra_audience +# secret: false +# description: Entra ID audience for the Outlook Mail MCP server +# - name: outlook_mail_entra_mcp_target +# secret: false +# description: URL of the Outlook Mail MCP server that accepts user Entra tokens resources: - kind: model id: gpt-4.1-mini name: AZURE_AI_MODEL_DEPLOYMENT_NAME - - kind: toolbox - name: agent-tools - tools: - - type: web_search - name: web_search - - type: code_interpreter - name: code_interpreter - + # - kind: connection + # # A connection that uses a GitHub Personal Access Token (PAT) to authenticate with the GitHub MCP server + # name: github-mcp-pat-conn + # category: RemoteTool + # authType: CustomKeys + # target: https://api.githubcopilot.com/mcp + # credentials: + # type: CustomKeys + # keys: + # Authorization: "Bearer {{ github_pat }}" + # - kind: connection + # # A connection that uses OAuth2 to authenticate with the GitHub MCP server + # name: github-mcp-oauth-conn + # category: RemoteTool + # authType: OAuth2 + # target: https://api.githubcopilot.com/mcp + # connectorName: foundrygithubmcp + # credentials: + # type: OAuth2 + # clientId: managed + # clientSecret: managed + # - kind: connection + # name: language-mcp-conn + # category: RemoteTool + # authType: AgenticIdentity + # audience: "{{ language_mcp_entra_audience }}" + # target: "{{ language_mcp_target_url }}" + # # - kind: connection + # # name: outlook-mail-conn + # # category: RemoteTool + # # authType: UserEntraToken + # # audience: "{{ outlook_mail_entra_audience }}" + # # target: "{{ outlook_mail_entra_mcp_target }}" + # - kind: toolbox + # name: agent-tools + # tools: + # - type: web_search + # name: web_search + # - type: code_interpreter + # name: code_interpreter + # # - type: mcp + # # # This MCP tool doesn't require authentication + # # server_label: noauth_mcp + # # server_url: "{{ mcp_endpoint }}" + # # require_approval: "never" + # - type: mcp + # # This MCP tool uses the GitHub MCP server with a PAT for authentication or OAuth2 + # server_label: github + # project_connection_id: github-mcp-pat-conn # use `github-mcp-oauth-conn` for OAuth2 authentication + # require_approval: "never" + # - type: mcp + # # This MCP tool uses the Azure Language MCP server with agent identity for authentication + # server_label: language-mcp + # project_connection_id: language-mcp-conn + # require_approval: "never" + # # - type: mcp + # # server_label: outlook-mail + # # project_connection_id: outlook-mail-conn + # # require_approval: "never" diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py index c836ae0ec1..c9f13109bc 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/main.py @@ -3,12 +3,11 @@ import asyncio import os from collections.abc import Callable -from typing import Any +import httpx from agent_framework import Agent, MCPStreamableHTTPTool from agent_framework.foundry import FoundryChatClient from agent_framework_foundry_hosting import ResponsesHostServer -from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider from dotenv import load_dotenv @@ -16,7 +15,7 @@ from dotenv import load_dotenv load_dotenv() -def _resolve_toolbox_endpoint() -> str: +def resolve_toolbox_endpoint() -> str: """Resolve the toolbox MCP endpoint URL. Prefers the explicit ``FOUNDRY_TOOLBOX_ENDPOINT`` env var; falls back to @@ -29,47 +28,61 @@ def _resolve_toolbox_endpoint() -> str: return endpoint project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/") toolbox_name = os.environ["TOOLBOX_NAME"] - return f"{project_endpoint}/toolsets/{toolbox_name}/mcp?api-version=v1" + return f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1" -def make_toolbox_header_provider(credential: TokenCredential) -> Callable[[dict[str, Any]], dict[str, str]]: - """Build a header_provider that injects a fresh Azure AI bearer token on every MCP request.""" - get_token = get_bearer_token_provider(credential, "https://ai.azure.com/.default") +class ToolboxAuth(httpx.Auth): + """Injects a fresh bearer token on every request.""" - def provide(_kwargs: dict[str, Any]) -> dict[str, str]: - return { - "Authorization": f"Bearer {get_token()}", - } + def __init__(self, token_provider: Callable[[], str]): + self._get_token = token_provider - return provide + def auth_flow(self, request: httpx.Request): + request.headers["Authorization"] = f"Bearer {self._get_token()}" + yield request async def main(): credential = DefaultAzureCredential() - client = FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=credential, - ) + # Create the toolbox + token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default") - toolbox_tool = MCPStreamableHTTPTool( - name="foundry_toolbox", - description="Tools exposed by the configured Foundry toolbox", - url=_resolve_toolbox_endpoint(), - header_provider=make_toolbox_header_provider(credential), - load_prompts=False, - ) + # Resolve the endpoint once and derive the tool name from the same source: when + # ``TOOLBOX_NAME`` isn't explicitly set, parse it out of the resolved URL so the + # tool's local name and the upstream toolbox always agree. + toolbox_endpoint = resolve_toolbox_endpoint() + toolbox_name = os.environ.get("TOOLBOX_NAME") or toolbox_endpoint.rsplit("/mcp", 1)[0].rsplit("/", 1)[-1] + + async with httpx.AsyncClient( + auth=ToolboxAuth(token_provider), + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + timeout=120.0, + ) as http_client: + toolbox = MCPStreamableHTTPTool( + name=toolbox_name, + url=toolbox_endpoint, + http_client=http_client, + load_prompts=False, + ) + + # Create the chat client + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=credential, + ) + + agent = Agent( + client=client, + instructions="You are a friendly assistant. Keep your answers brief.", + tools=toolbox, + # History will be managed by the hosting infrastructure, thus there + # is no need to store history by the service. Learn more at: + # https://developers.openai.com/api/reference/resources/responses/methods/create + default_options={"store": False}, + ) - async with Agent( - client=client, - instructions="You are a friendly assistant. Keep your answers brief.", - tools=toolbox_tool, - # History will be managed by the hosting infrastructure, thus there - # is no need to store history by the service. Learn more at: - # https://developers.openai.com/api/reference/resources/responses/methods/create - default_options={"store": False}, - ) as agent: server = ResponsesHostServer(agent) await server.run_async() diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/requirements.txt b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/requirements.txt index 1ed4f3c7d4..eaa894b7c4 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/requirements.txt +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/04_foundry_toolbox/requirements.txt @@ -1,2 +1,4 @@ -agent-framework -agent-framework-foundry-hosting +# agent-framework +# agent-framework-foundry-hosting + +mcp>=1.24.0,<2 diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md index d68ddc16c2..82005970f5 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/README.md @@ -21,9 +21,10 @@ This agent uses four tools: 1. **Get Current Working Directory Tool (`get_cwd`)** – Returns the current working directory of the agent host process. 2. **List Files Tool (`list_files`)** – Lists the files in a specified directory. 3. **Read File Tool (`read_file`)** – Reads the contents of a specified file. -4. **Code Interpreter Tool (`code_interpreter`)** – Allows the agent to execute Python code in a safe. +4. **Code Interpreter Tool (`code_interpreter`)** – Allows the agent to execute Python code in a safe sandboxed environment. +5. **Web Search Tool (`web_search`)** – Allows the agent to perform web searches using the Bing Search API. -> In this sample, the filesystem tools are function tools defined in Python using the `@tool` decorator from the Agent Framework. The code interpreter tool is a managed tool provided by [Foundry Toolbox](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox). Learn more about foundry toolbox integration with hosted agents with this [sample](../04_foundry_toolbox/). +> In this sample, the filesystem tools are function tools defined in Python using the `@tool` decorator from the Agent Framework. The code interpreter tool and web search tool are managed tools provided by [Foundry Toolbox](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/toolbox). Learn more about foundry toolbox integration with hosted agents with this [sample](../04_foundry_toolbox/). ## Running the Agent Host @@ -34,20 +35,20 @@ An extra environment variable must be set to point to the toolbox MCP endpoint. **Option A – Set `FOUNDRY_TOOLBOX_ENDPOINT` directly** (recommended for local development): ```bash -export FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolsets//mcp?api-version=v1" +export FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolboxes//mcp?api-version=v1" ``` Or in PowerShell: ```powershell -$env:FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolsets//mcp?api-version=v1" +$env:FOUNDRY_TOOLBOX_ENDPOINT="https://.services.ai.azure.com/api/projects//toolboxes//mcp?api-version=v1" ``` **Option B – Set `TOOLBOX_NAME`** (used automatically by the Foundry hosting scaffolding after `azd provision`): The agent derives the endpoint at runtime as: ``` -{FOUNDRY_PROJECT_ENDPOINT}/toolsets/{TOOLBOX_NAME}/mcp?api-version=v1 +{FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{TOOLBOX_NAME}/mcp?api-version=v1 ``` When deployed via `azd provision`, the scaffolding injects `TOOLBOX_NAME=agent-tools` and `FOUNDRY_PROJECT_ENDPOINT` automatically from the provisioned resources declared in [`agent.manifest.yaml`](agent.manifest.yaml). diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py index a324ab16a1..06c35efd87 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/06_files/main.py @@ -3,12 +3,11 @@ import asyncio import os from collections.abc import Callable -from typing import Any +import httpx from agent_framework import Agent, MCPStreamableHTTPTool, tool from agent_framework.foundry import FoundryChatClient from agent_framework_foundry_hosting import ResponsesHostServer -from azure.core.credentials import TokenCredential from azure.identity import DefaultAzureCredential, get_bearer_token_provider from dotenv import load_dotenv @@ -16,7 +15,7 @@ from dotenv import load_dotenv load_dotenv() -def _resolve_toolbox_endpoint() -> str: +def resolve_toolbox_endpoint() -> str: """Resolve the toolbox MCP endpoint URL. Prefers the explicit ``FOUNDRY_TOOLBOX_ENDPOINT`` env var; falls back to @@ -29,19 +28,18 @@ def _resolve_toolbox_endpoint() -> str: return endpoint project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/") toolbox_name = os.environ["TOOLBOX_NAME"] - return f"{project_endpoint}/toolsets/{toolbox_name}/mcp?api-version=v1" + return f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1" -def make_toolbox_header_provider(credential: TokenCredential) -> Callable[[dict[str, Any]], dict[str, str]]: - """Build a header_provider that injects a fresh Azure AI bearer token on every MCP request.""" - get_token = get_bearer_token_provider(credential, "https://ai.azure.com/.default") +class ToolboxAuth(httpx.Auth): + """Injects a fresh bearer token on every request.""" - def provide(_kwargs: dict[str, Any]) -> dict[str, str]: - return { - "Authorization": f"Bearer {get_token()}", - } + def __init__(self, token_provider: Callable[[], str]): + self._get_token = token_provider - return provide + def auth_flow(self, request: httpx.Request): + request.headers["Authorization"] = f"Bearer {self._get_token()}" + yield request @tool(description="Get the current working directory.", approval_mode="never_require") @@ -75,39 +73,47 @@ def read_file(file_path: str) -> str: async def main(): credential = DefaultAzureCredential() - client = FoundryChatClient( - project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=credential, - ) + # Create the toolbox + token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default") - # Connect to the toolbox MCP endpoint and expose only the code_interpreter tool. - # The toolbox deployed has two tools: (see agent.manifest.yaml) - # - `code_interpreter` - # - `web_search` - # We only need the `code_interpreter` tool for this sample. - toolbox_tool = MCPStreamableHTTPTool( - name="foundry_toolbox", - description="Tools exposed by the configured Foundry toolbox", - url=_resolve_toolbox_endpoint(), - header_provider=make_toolbox_header_provider(credential), - load_prompts=False, - allowed_tools=["code_interpreter"], - ) + # Resolve the endpoint once and derive the tool name from the same source: when + # ``TOOLBOX_NAME`` isn't explicitly set, parse it out of the resolved URL so the + # tool's local name and the upstream toolbox always agree. + toolbox_endpoint = resolve_toolbox_endpoint() + toolbox_name = os.environ.get("TOOLBOX_NAME") or toolbox_endpoint.rsplit("/mcp", 1)[0].rsplit("/", 1)[-1] - async with Agent( - client=client, - instructions=( - "You are a friendly assistant. Keep your answers brief. " - "Make sure all mathematical calculations are performed using the code interpreter " - "instead of mental arithmetic." - ), - tools=[get_cwd, list_files, read_file, toolbox_tool], - # History will be managed by the hosting infrastructure, thus there - # is no need to store history by the service. Learn more at: - # https://developers.openai.com/api/reference/resources/responses/methods/create - default_options={"store": False}, - ) as agent: + async with httpx.AsyncClient( + auth=ToolboxAuth(token_provider), + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + timeout=120.0, + ) as http_client: + toolbox = MCPStreamableHTTPTool( + name=toolbox_name, + url=toolbox_endpoint, + http_client=http_client, + load_prompts=False, + ) + + # Create the chat client + client = FoundryChatClient( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=credential, + ) + + agent = Agent( + client=client, + instructions=( + "You are a friendly assistant. Keep your answers brief. " + "Make sure all mathematical calculations are performed using the code interpreter " + "instead of mental arithmetic." + ), + tools=[get_cwd, list_files, read_file, toolbox], + # History will be managed by the hosting infrastructure, thus there + # is no need to store history by the service. Learn more at: + # https://developers.openai.com/api/reference/resources/responses/methods/create + default_options={"store": False}, + ) server = ResponsesHostServer(agent) await server.run_async() From 01a3c5be8afc119b1bcbaed011015d9186241de3 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Wed, 20 May 2026 23:10:32 +0100 Subject: [PATCH 16/22] ci: pin third-party GitHub Actions to commit SHAs (#5972) Replaces every floating tag in our workflow and composite action files with an immutable 40-character commit SHA, keeping the original `# vX` comment so Dependabot can still propose version bumps. 186 occurrences across 25 workflows and 2 composite actions. Also widens the github-actions Dependabot entry to use the plural `directories` key with `/.github/actions/*` so composite actions under `.github/actions//action.yml` are kept up to date. Previously Dependabot only scanned `.github/workflows` and the repo-root `action.yml`, leaving our `python-setup` and `sample-validation-setup` composite actions unmaintained. --- .github/actions/python-setup/action.yml | 2 +- .../sample-validation-setup/action.yml | 4 +- .github/dependabot.yml | 12 ++- .github/workflows/codeql-analysis.yml | 8 +- .github/workflows/devflow-pr-review.yml | 10 +-- .github/workflows/dotnet-build-and-test.yml | 48 ++++++------ .github/workflows/dotnet-format.yml | 4 +- .../workflows/dotnet-integration-tests.yml | 6 +- .github/workflows/dotnet-verify-samples.yml | 8 +- .github/workflows/issue-triage.yml | 14 ++-- .github/workflows/label-issues.yml | 2 +- .github/workflows/label-pr.yml | 2 +- .github/workflows/label-title-prefix.yml | 2 +- .github/workflows/markdown-link-check.yml | 4 +- .github/workflows/merge-gatekeeper.yml | 2 +- .github/workflows/python-code-quality.yml | 12 +-- .../python-dependency-range-validation.yml | 6 +- .../python-dev-dependency-upgrade.yml | 2 +- .github/workflows/python-docs.yml | 4 +- .../workflows/python-integration-tests.yml | 54 +++++++------- .github/workflows/python-lab-tests.yml | 8 +- .github/workflows/python-merge-tests.yml | 74 +++++++++---------- .github/workflows/python-release.yml | 4 +- .../workflows/python-sample-validation.yml | 74 +++++++++---------- .../workflows/python-test-coverage-report.yml | 6 +- .github/workflows/python-test-coverage.yml | 4 +- .github/workflows/python-tests.yml | 4 +- .github/workflows/stale-issue-pr-ping.yml | 4 +- 28 files changed, 195 insertions(+), 189 deletions(-) diff --git a/.github/actions/python-setup/action.yml b/.github/actions/python-setup/action.yml index ed595ee87a..6cbe1cb833 100644 --- a/.github/actions/python-setup/action.yml +++ b/.github/actions/python-setup/action.yml @@ -17,7 +17,7 @@ runs: using: "composite" steps: - name: Set up uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 with: version-file: "python/pyproject.toml" enable-cache: true diff --git a/.github/actions/sample-validation-setup/action.yml b/.github/actions/sample-validation-setup/action.yml index 14c92694ff..c9d2d2d6ac 100644 --- a/.github/actions/sample-validation-setup/action.yml +++ b/.github/actions/sample-validation-setup/action.yml @@ -24,7 +24,7 @@ runs: using: "composite" steps: - name: Set up Node.js environment - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 @@ -37,7 +37,7 @@ runs: run: copilot --version && copilot -p "What can you do in one sentence?" - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ inputs.azure-client-id }} tenant-id: ${{ inputs.azure-tenant-id }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 90b127a829..22db68fc60 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,9 +44,15 @@ updates: # Maintain dependencies for github-actions - package-ecosystem: "github-actions" - # Workflow files stored in the - # default location of `.github/workflows` - directory: "/" + # Cover both the standard workflow location and our composite actions. + # With `directory: "/"` Dependabot only scans `.github/workflows/*.{yml,yaml}` + # plus a root-level `action.yml/action.yaml`. It does NOT recurse into + # `.github/actions/*/action.yml`, so the glob below is required to keep the + # composite actions in `.github/actions//` up to date as well. + # Ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#directories-or-directory-- + directories: + - "/" + - "/.github/actions/*" schedule: interval: "weekly" day: "sunday" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 21d3aa2ed0..361b591e76 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,13 +32,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -51,7 +51,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -64,6 +64,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/devflow-pr-review.yml b/.github/workflows/devflow-pr-review.yml index 5ce9592a51..ca6a20ddb2 100644 --- a/.github/workflows/devflow-pr-review.yml +++ b/.github/workflows/devflow-pr-review.yml @@ -66,7 +66,7 @@ jobs: - name: Check PR author team membership id: check - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }} PR_NUMBER: ${{ steps.pr.outputs.pr_number }} @@ -116,7 +116,7 @@ jobs: steps: # Safe checkout: base repo only, not the untrusted PR head. - name: Checkout target repo base - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }} fetch-depth: 0 @@ -125,7 +125,7 @@ jobs: # Private DevFlow checkout: the PAT/token grants access to this repo's code. - name: Checkout DevFlow - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: repository: ${{ env.DEVFLOW_REPOSITORY }} ref: ${{ env.DEVFLOW_REF }} @@ -135,12 +135,12 @@ jobs: path: devflow - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - name: Set up uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.x" enable-cache: true diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index c639da53d5..8fe1fbf176 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -41,8 +41,8 @@ jobs: functionsChanged: ${{ steps.filter.outputs.functions }} coreChanged: ${{ steps.filter.outputs.core }} steps: - - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3 id: filter with: filters: | @@ -111,7 +111,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false sparse-checkout: | @@ -122,7 +122,7 @@ jobs: declarative-agents - name: Setup dotnet - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: global-json-file: ${{ github.workspace }}/dotnet/global.json - name: Build dotnet solutions @@ -181,7 +181,7 @@ jobs: runs-on: ${{ matrix.os }} environment: ${{ matrix.environment }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false sparse-checkout: | @@ -202,7 +202,7 @@ jobs: echo "COSMOSDB_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV - name: Setup dotnet - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: global-json-file: ${{ github.workspace }}/dotnet/global.json @@ -271,7 +271,7 @@ jobs: - name: Azure CLI Login if: github.event_name != 'pull_request' && matrix.integration-tests - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -318,7 +318,7 @@ jobs: # Generate test reports and check coverage - name: Generate test reports if: matrix.targetFramework == env.COVERAGE_FRAMEWORK - uses: danielpalme/ReportGenerator-GitHub-Action@5.5.3 + uses: danielpalme/ReportGenerator-GitHub-Action@2a82782178b2816d9d6960a7345fdd164791b323 # 5.5.3 with: reports: "./TestResults/Coverage/**/*.cobertura.xml" targetdir: "./TestResults/Reports" @@ -326,7 +326,7 @@ jobs: - name: Upload coverage report artifact if: matrix.targetFramework == env.COVERAGE_FRAMEWORK - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: CoverageReport-${{ matrix.os }}-${{ matrix.targetFramework }}-${{ matrix.configuration }} # Artifact name path: ./TestResults/Reports # Directory containing files to upload @@ -338,7 +338,7 @@ jobs: - name: Upload integration test results if: always() && github.event_name != 'pull_request' && matrix.integration-tests - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dotnet-test-results-${{ matrix.targetFramework }}-${{ matrix.os }} path: IntegrationTestResults/**/*.junit @@ -356,7 +356,7 @@ jobs: env: configuration: Release steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false sparse-checkout: | @@ -366,7 +366,7 @@ jobs: python - name: Setup dotnet - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: global-json-file: ${{ github.workspace }}/dotnet/global.json @@ -381,7 +381,7 @@ jobs: run: dotnet build dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj -c "$configuration" --warnaserror - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -442,7 +442,7 @@ jobs: runs-on: ubuntu-latest environment: integration steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false sparse-checkout: | @@ -453,7 +453,7 @@ jobs: declarative-agents - name: Setup dotnet - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: global-json-file: ${{ github.workspace }}/dotnet/global.json @@ -465,7 +465,7 @@ jobs: dotnet build ./tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests -c Release -f net10.0 --warnaserror - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -522,7 +522,7 @@ jobs: - name: Upload functions test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dotnet-test-results-functions-net10.0-ubuntu-latest path: IntegrationTestResults/**/*.junit @@ -560,14 +560,14 @@ jobs: - name: Fail workflow if tests failed id: check_tests_failed if: contains(join(needs.*.result, ','), 'failure') - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: core.setFailed('Integration Tests Failed!') - name: Fail workflow if tests cancelled id: check_tests_cancelled if: contains(join(needs.*.result, ','), 'cancelled') - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: core.setFailed('Integration Tests Cancelled!') @@ -585,7 +585,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false sparse-checkout: | @@ -597,12 +597,12 @@ jobs: python-version: "3.13" os: ${{ runner.os }} - name: Download all test results from current run - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: dotnet-test-results-* path: dotnet-test-results/ - name: Restore report history cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: python/dotnet-integration-report-history.json key: dotnet-integration-report-history-${{ github.run_id }} @@ -619,13 +619,13 @@ jobs: run: cat dotnet-integration-test-report.md >> $GITHUB_STEP_SUMMARY - name: Save report history cache if: always() - uses: actions/cache/save@v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: python/dotnet-integration-report-history.json key: dotnet-integration-report-history-${{ github.run_id }} - name: Upload trend report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dotnet-integration-test-report path: | diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 8bdaeba8a3..b9672967ef 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 persist-credentials: false @@ -42,7 +42,7 @@ jobs: - name: Get changed files id: changed-files if: github.event_name == 'pull_request' - uses: jitterbit/get-changed-files@v1 + uses: jitterbit/get-changed-files@b17fbb00bdc0c0f63fcf166580804b4d2cdc2a42 # v1 continue-on-error: true - name: No C# files changed diff --git a/.github/workflows/dotnet-integration-tests.yml b/.github/workflows/dotnet-integration-tests.yml index 3aedbacd1a..5b08752abb 100644 --- a/.github/workflows/dotnet-integration-tests.yml +++ b/.github/workflows/dotnet-integration-tests.yml @@ -29,7 +29,7 @@ jobs: environment: integration timeout-minutes: 60 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -50,7 +50,7 @@ jobs: echo "COSMOS_EMULATOR_AVAILABLE=true" >> $env:GITHUB_ENV - name: Setup dotnet - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: global-json-file: ${{ github.workspace }}/dotnet/global.json @@ -63,7 +63,7 @@ jobs: done - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/dotnet-verify-samples.yml b/.github/workflows/dotnet-verify-samples.yml index 7cb0b9636f..3552e1e3af 100644 --- a/.github/workflows/dotnet-verify-samples.yml +++ b/.github/workflows/dotnet-verify-samples.yml @@ -41,7 +41,7 @@ jobs: environment: 'integration' timeout-minutes: 90 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false sparse-checkout: | @@ -52,13 +52,13 @@ jobs: declarative-agents - name: Setup dotnet - uses: actions/setup-dotnet@v5.2.0 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: global-json-file: ${{ github.workspace }}/dotnet/global.json - name: Azure CLI Login if: github.event_name != 'pull_request' - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -123,7 +123,7 @@ jobs: - name: Upload results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: verify-samples-results path: | diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index ecab04d7e5..a7d0ab647d 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -53,7 +53,7 @@ jobs: echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT" - name: Checkout scripts - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: sparse-checkout: .github/scripts fetch-depth: 1 @@ -61,7 +61,7 @@ jobs: - name: Check issue author team membership id: check - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: TEAM_NAME: ${{ secrets.DEVELOPER_TEAM }} ISSUE_NUMBER: ${{ steps.issue.outputs.issue_number }} @@ -93,7 +93,7 @@ jobs: steps: # Safe checkout: base repo only. - name: Checkout target repo base - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 persist-credentials: false @@ -101,7 +101,7 @@ jobs: # Private DevFlow (maf-dashboard) checkout. - name: Checkout DevFlow - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: repository: ${{ env.DEVFLOW_REPOSITORY }} ref: ${{ env.DEVFLOW_REF }} @@ -111,12 +111,12 @@ jobs: path: devflow - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.13" - name: Set up uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version: "0.11.x" enable-cache: true @@ -126,7 +126,7 @@ jobs: run: uv sync --frozen - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml index 111c63ef13..31409df630 100644 --- a/.github/workflows/label-issues.yml +++ b/.github/workflows/label-issues.yml @@ -13,7 +13,7 @@ jobs: permissions: issues: write steps: - - uses: actions/github-script@v8 + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} script: | diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 4aea432e31..7d0282b916 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -16,6 +16,6 @@ jobs: pull-requests: write steps: - - uses: actions/labeler@v6 + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6 with: repo-token: "${{ secrets.GH_ACTIONS_PR_WRITE }}" diff --git a/.github/workflows/label-title-prefix.yml b/.github/workflows/label-title-prefix.yml index b8d5b762a7..8457e8e428 100644 --- a/.github/workflows/label-title-prefix.yml +++ b/.github/workflows/label-title-prefix.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - - uses: actions/github-script@v8 + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 name: "Issue/PR: update title" with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml index 5c984c5796..0e59e4254f 100644 --- a/.github/workflows/markdown-link-check.yml +++ b/.github/workflows/markdown-link-check.yml @@ -19,13 +19,13 @@ jobs: runs-on: ubuntu-22.04 # check out the latest version of the code steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false # Checks the status of hyperlinks in all files - name: Run linkspector - uses: umbrelladocs/action-linkspector@v1 + uses: umbrelladocs/action-linkspector@963b6264d7de32c904942a70b488d3407453049e # v1 with: reporter: local filter_mode: nofilter diff --git a/.github/workflows/merge-gatekeeper.yml b/.github/workflows/merge-gatekeeper.yml index a9429326ed..52adbcb8e4 100644 --- a/.github/workflows/merge-gatekeeper.yml +++ b/.github/workflows/merge-gatekeeper.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Wait for required checks if: github.event_name == 'pull_request' - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: TIMEOUT_SECONDS: "3600" INTERVAL_SECONDS: "30" diff --git a/.github/workflows/python-code-quality.yml b/.github/workflows/python-code-quality.yml index ef75293f0c..6527a89cd8 100644 --- a/.github/workflows/python-code-quality.yml +++ b/.github/workflows/python-code-quality.yml @@ -27,7 +27,7 @@ jobs: env: UV_PYTHON: ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Set up python and install the project @@ -38,11 +38,11 @@ jobs: os: ${{ runner.os }} env: UV_CACHE_DIR: /tmp/.uv-cache - - uses: actions/cache@v5 + - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: ~/.cache/prek key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }} - - uses: j178/prek-action@v1 + - uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1 name: Run Pre-commit Hooks (excluding poe-check) env: SKIP: poe-check @@ -64,7 +64,7 @@ jobs: env: UV_PYTHON: ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Set up python and install the project @@ -93,7 +93,7 @@ jobs: env: UV_PYTHON: ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Set up python and install the project @@ -124,7 +124,7 @@ jobs: env: UV_PYTHON: ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: Set up python and install the project diff --git a/.github/workflows/python-dependency-range-validation.yml b/.github/workflows/python-dependency-range-validation.yml index 692c94101e..67c8d92bc8 100644 --- a/.github/workflows/python-dependency-range-validation.yml +++ b/.github/workflows/python-dependency-range-validation.yml @@ -22,7 +22,7 @@ jobs: UV_PYTHON: "3.13" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 @@ -44,7 +44,7 @@ jobs: - name: Upload dependency range report # Always publish the report so failures are inspectable even when validation fails. if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dependency-range-results path: python/scripts/dependencies/dependency-range-results.json @@ -53,7 +53,7 @@ jobs: - name: Create issues for failed dependency candidates # Always process the report so failed candidates create actionable tracking issues. if: always() - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const fs = require("fs") diff --git a/.github/workflows/python-dev-dependency-upgrade.yml b/.github/workflows/python-dev-dependency-upgrade.yml index 0dcd138b25..dc55da9227 100644 --- a/.github/workflows/python-dev-dependency-upgrade.yml +++ b/.github/workflows/python-dev-dependency-upgrade.yml @@ -18,7 +18,7 @@ jobs: UV_PYTHON: "3.13" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 diff --git a/.github/workflows/python-docs.yml b/.github/workflows/python-docs.yml index f962ec318f..6ea3443f55 100644 --- a/.github/workflows/python-docs.yml +++ b/.github/workflows/python-docs.yml @@ -24,9 +24,9 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: version-file: "python/pyproject.toml" enable-cache: true diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 0d8ae36b8f..3073a71636 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -36,7 +36,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -69,7 +69,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -90,7 +90,7 @@ jobs: --junitxml=pytest.xml - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-openai path: ./python/pytest.xml @@ -112,7 +112,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -123,7 +123,7 @@ jobs: python-version: ${{ env.UV_PYTHON }} os: ${{ runner.os }} - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -141,7 +141,7 @@ jobs: --junitxml=pytest.xml - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-azure-openai path: ./python/pytest.xml @@ -163,7 +163,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -177,7 +177,7 @@ jobs: run: curl -fsSL https://ollama.com/install.sh | sh working-directory: . - name: Cache Ollama models - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ~/.ollama/models key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1 @@ -231,7 +231,7 @@ jobs: --junitxml=pytest.xml - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-misc path: ./python/pytest.xml @@ -283,7 +283,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -294,7 +294,7 @@ jobs: python-version: ${{ env.UV_PYTHON }} os: ${{ runner.os }} - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -315,7 +315,7 @@ jobs: --junitxml=pytest.xml - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-functions path: ./python/pytest.xml @@ -341,7 +341,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -352,7 +352,7 @@ jobs: python-version: ${{ env.UV_PYTHON }} os: ${{ runner.os }} - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -369,7 +369,7 @@ jobs: --junitxml=pytest.xml - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-foundry path: ./python/pytest.xml @@ -388,7 +388,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -399,7 +399,7 @@ jobs: python-version: ${{ env.UV_PYTHON }} os: ${{ runner.os }} - name: Azure CLI Login - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -416,7 +416,7 @@ jobs: --junitxml=pytest.xml - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-foundry-hosting path: ./python/pytest.xml @@ -443,7 +443,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -468,7 +468,7 @@ jobs: run: uv run --directory packages/azure-cosmos poe integration-tests -n logical --dist worksteal --timeout=120 --session-timeout=900 --timeout_method thread --retries 2 --retry-delay 5 --junitxml=${{ github.workspace }}/python/pytest.xml - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-cosmos path: ./python/pytest.xml @@ -496,7 +496,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.checkout-ref }} persist-credentials: false @@ -506,12 +506,12 @@ jobs: python-version: ${{ env.UV_PYTHON }} os: ${{ runner.os }} - name: Download all test results from current run - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: test-results-* path: test-results/ - name: Restore report history cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: python/integration-report-history.json key: integration-report-history-integration-${{ github.run_id }} @@ -528,13 +528,13 @@ jobs: run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY - name: Save report history cache if: always() - uses: actions/cache/save@v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: python/integration-report-history.json key: integration-report-history-integration-${{ github.run_id }} - name: Upload unified trend report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: integration-test-report path: | @@ -558,12 +558,12 @@ jobs: steps: - name: Fail workflow if tests failed if: contains(join(needs.*.result, ','), 'failure') - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: core.setFailed('Integration Tests Failed!') - name: Fail workflow if tests cancelled if: contains(join(needs.*.result, ','), 'cancelled') - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: core.setFailed('Integration Tests Cancelled!') diff --git a/.github/workflows/python-lab-tests.yml b/.github/workflows/python-lab-tests.yml index 0c11cf1a58..3f959f85c2 100644 --- a/.github/workflows/python-lab-tests.yml +++ b/.github/workflows/python-lab-tests.yml @@ -24,8 +24,8 @@ jobs: outputs: pythonChanges: ${{ steps.filter.outputs.python}} steps: - - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3 id: filter with: filters: | @@ -59,7 +59,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup @@ -94,7 +94,7 @@ jobs: # Surface failing tests - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/packages/lab/**.xml summary: true diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml index ea20ad06e8..919c320c08 100644 --- a/.github/workflows/python-merge-tests.yml +++ b/.github/workflows/python-merge-tests.yml @@ -41,8 +41,8 @@ jobs: foundryHostingChanged: ${{ steps.filter.outputs.foundry_hosting }} cosmosChanged: ${{ steps.filter.outputs.cosmos }} steps: - - uses: actions/checkout@v6 - - uses: dorny/paths-filter@v3 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3 id: filter with: filters: | @@ -106,7 +106,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -123,7 +123,7 @@ jobs: working-directory: ./python - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -153,7 +153,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -177,7 +177,7 @@ jobs: working-directory: ./python - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -186,7 +186,7 @@ jobs: title: OpenAI integration test results - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-openai path: ./python/pytest.xml @@ -214,7 +214,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -223,7 +223,7 @@ jobs: os: ${{ runner.os }} - name: Azure CLI Login if: github.event_name != 'pull_request' - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -247,7 +247,7 @@ jobs: working-directory: ./python - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -256,7 +256,7 @@ jobs: title: Azure OpenAI integration test results - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-azure-openai path: ./python/pytest.xml @@ -284,7 +284,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -295,7 +295,7 @@ jobs: run: curl -fsSL https://ollama.com/install.sh | sh working-directory: . - name: Cache Ollama models - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ~/.ollama/models key: ollama-models-qwen2.5-1.5b-nomic-embed-text-v1 @@ -370,7 +370,7 @@ jobs: kill -KILL -- "-$server_pid" 2>/dev/null || kill -KILL "$server_pid" 2>/dev/null || true - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -379,7 +379,7 @@ jobs: title: Misc integration test results - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-misc path: ./python/pytest.xml @@ -417,7 +417,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -426,7 +426,7 @@ jobs: os: ${{ runner.os }} - name: Azure CLI Login if: github.event_name != 'pull_request' - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -448,7 +448,7 @@ jobs: working-directory: ./python - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -457,7 +457,7 @@ jobs: title: Functions integration test results - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-functions path: ./python/pytest.xml @@ -488,7 +488,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -497,7 +497,7 @@ jobs: os: ${{ runner.os }} - name: Azure CLI Login if: github.event_name != 'pull_request' - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -515,7 +515,7 @@ jobs: working-directory: ./python - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -524,7 +524,7 @@ jobs: title: Test results - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-foundry path: ./python/pytest.xml @@ -549,7 +549,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -558,7 +558,7 @@ jobs: os: ${{ runner.os }} - name: Azure CLI Login if: github.event_name != 'pull_request' - uses: azure/login@v2 + uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -576,7 +576,7 @@ jobs: working-directory: ./python - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -585,7 +585,7 @@ jobs: title: Foundry Hosting integration test results - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-foundry-hosting path: ./python/pytest.xml @@ -620,7 +620,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -643,7 +643,7 @@ jobs: working-directory: ./python - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true @@ -652,7 +652,7 @@ jobs: title: Cosmos integration test results - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-cosmos path: ./python/pytest.xml @@ -680,19 +680,19 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project uses: ./.github/actions/python-setup with: python-version: ${{ env.UV_PYTHON }} os: ${{ runner.os }} - name: Download all test results from current run - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: test-results-* path: test-results/ - name: Restore report history cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: python/integration-report-history.json key: integration-report-history-merge-${{ github.run_id }} @@ -709,13 +709,13 @@ jobs: run: cat integration-test-report.md >> $GITHUB_STEP_SUMMARY - name: Save report history cache if: always() - uses: actions/cache/save@v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: python/integration-report-history.json key: integration-report-history-merge-${{ github.run_id }} - name: Upload unified trend report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: integration-test-report path: | @@ -740,13 +740,13 @@ jobs: - name: Fail workflow if tests failed id: check_tests_failed if: contains(join(needs.*.result, ','), 'failure') - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: core.setFailed('Integration Tests Failed!') - name: Fail workflow if tests cancelled id: check_tests_cancelled if: contains(join(needs.*.result, ','), 'cancelled') - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: core.setFailed('Integration Tests Cancelled!') diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index ba6e3689b0..b618dce246 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -23,7 +23,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -56,7 +56,7 @@ jobs: - name: Build the package run: uv run poe --directory packages/${{ env.PACKAGE }} build - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: | python/dist/* diff --git a/.github/workflows/python-sample-validation.yml b/.github/workflows/python-sample-validation.yml index 8b72df3b74..bd76eb12d2 100644 --- a/.github/workflows/python-sample-validation.yml +++ b/.github/workflows/python-sample-validation.yml @@ -29,7 +29,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -49,7 +49,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 01-get-started --save-report --report-name 01-get-started - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-01-get-started @@ -82,7 +82,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -111,7 +111,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents --exclude providers --save-report --report-name 02-agents - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents @@ -130,7 +130,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -152,7 +152,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/openai --save-report --report-name 02-agents-openai - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-openai @@ -170,7 +170,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -191,7 +191,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/azure --save-report --report-name 02-agents-azure - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-azure @@ -208,7 +208,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -228,7 +228,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/anthropic --save-report --report-name 02-agents-anthropic - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-anthropic @@ -242,7 +242,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -257,7 +257,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/github_copilot --save-report --report-name 02-agents-github-copilot - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-github-copilot @@ -274,7 +274,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -289,7 +289,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/amazon --save-report --report-name 02-agents-amazon - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-amazon @@ -306,7 +306,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -321,7 +321,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/ollama --save-report --report-name 02-agents-ollama - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-ollama @@ -341,7 +341,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -363,7 +363,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/foundry --save-report --report-name 02-agents-foundry - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-foundry @@ -383,7 +383,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -405,7 +405,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/copilotstudio --save-report --report-name 02-agents-copilotstudio - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-copilotstudio @@ -419,7 +419,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -434,7 +434,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 02-agents/providers/custom --save-report --report-name 02-agents-custom - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-02-agents-custom @@ -451,7 +451,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -471,7 +471,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 03-workflows --save-report --report-name 03-workflows - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-03-workflows @@ -491,7 +491,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -506,7 +506,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 04-hosting --save-report --report-name 04-hosting - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-04-hosting @@ -534,7 +534,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -549,7 +549,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir 05-end-to-end --save-report --report-name 05-end-to-end - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-05-end-to-end @@ -574,7 +574,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -599,7 +599,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir autogen-migration --save-report --report-name autogen-migration - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-autogen-migration @@ -633,7 +633,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Setup environment uses: ./.github/actions/sample-validation-setup @@ -662,7 +662,7 @@ jobs: cd scripts && uv run python -m sample_validation --subdir semantic-kernel-migration --save-report --report-name semantic-kernel-migration - name: Upload validation report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-report-semantic-kernel-migration @@ -690,10 +690,10 @@ jobs: - validate-autogen-migration - validate-semantic-kernel-migration steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Download all validation reports - uses: actions/download-artifact@v7 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 with: pattern: validation-report-* path: reports/ @@ -701,7 +701,7 @@ jobs: - name: Restore validation history id: cache-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: validation-history/ key: validation-history-${{ github.run_id }} @@ -719,13 +719,13 @@ jobs: run: cat trend-report.md >> "$GITHUB_STEP_SUMMARY" - name: Save validation history - uses: actions/cache/save@v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: validation-history/ key: validation-history-${{ github.run_id }} - name: Upload trend report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: name: validation-trend-report diff --git a/.github/workflows/python-test-coverage-report.yml b/.github/workflows/python-test-coverage-report.yml index dbe5b9e9c0..f03967e72a 100644 --- a/.github/workflows/python-test-coverage-report.yml +++ b/.github/workflows/python-test-coverage-report.yml @@ -19,9 +19,9 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Download coverage report - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} run-id: ${{ github.event.workflow_run.id }} @@ -46,7 +46,7 @@ jobs: echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" - name: Pytest coverage comment id: coverageComment - uses: MishaKav/pytest-coverage-comment@v1.6.0 + uses: MishaKav/pytest-coverage-comment@26f986d2599c288bb62f623d29c2da98609e9cd4 # v1.6.0 with: github-token: ${{ secrets.GH_ACTIONS_PR_WRITE }} issue-number: ${{ env.PR_NUMBER }} diff --git a/.github/workflows/python-test-coverage.yml b/.github/workflows/python-test-coverage.yml index e14bcb30b8..16867fce09 100644 --- a/.github/workflows/python-test-coverage.yml +++ b/.github/workflows/python-test-coverage.yml @@ -22,7 +22,7 @@ jobs: env: UV_PYTHON: "3.11" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # Save the PR number to a file since the workflow_run event # in the coverage report workflow does not have access to it - name: Save PR number @@ -42,7 +42,7 @@ jobs: - name: Check coverage threshold run: python ${{ github.workspace }}/.github/workflows/python-check-coverage.py python-coverage.xml ${{ env.COVERAGE_THRESHOLD }} - name: Upload coverage report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: path: | python/python-coverage.xml diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 5530be9ffa..955fc9054d 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -27,7 +27,7 @@ jobs: run: working-directory: python steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up python and install the project id: python-setup uses: ./.github/actions/python-setup @@ -46,7 +46,7 @@ jobs: # Surface failing tests - name: Surface failing tests if: always() - uses: pmeier/pytest-results-action@v0.7.2 + uses: pmeier/pytest-results-action@20b595761ba9bf89e115e875f8bc863f913bc8ad # v0.7.2 with: path: ./python/pytest.xml summary: true diff --git a/.github/workflows/stale-issue-pr-ping.yml b/.github/workflows/stale-issue-pr-ping.yml index 483706fc76..8992c5928c 100644 --- a/.github/workflows/stale-issue-pr-ping.yml +++ b/.github/workflows/stale-issue-pr-ping.yml @@ -31,9 +31,9 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.13' From 47f5c3397f32d4333167f3a1e07493ed56c3ba24 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 21 May 2026 10:39:08 +0200 Subject: [PATCH 17/22] Python: feat(foundry): add experimental hosted tool factories on FoundryChatClient (#5958) * feat(foundry): add experimental hosted tool factories on FoundryChatClient Adds eight new `@experimental` static factory methods on `FoundryChatClient` covering Foundry-hosted tools that previously had no helper: - get_azure_ai_search_tool - get_sharepoint_tool - get_fabric_tool - get_memory_search_tool - get_computer_use_tool - get_browser_automation_tool - get_bing_custom_search_tool - get_a2a_tool All factories are marked with the new `ExperimentalFeature.FOUNDRY_TOOLS` tag and resolve the underlying `azure-ai-projects` preview classes lazily through a `_require_sdk_class` helper so older SDK versions still import cleanly and fail with a clear `ImportError` only on use. Tests cover each factory's return type and field wiring, the experimental metadata, and the missing-SDK-class fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(foundry): address review comments on tool-factory tests * Skip preview-tool tests gracefully (`_skip_if_sdk_class_missing`) when the installed `azure-ai-projects` does not expose the required preview class, matching the lazy-import guard in production code so the test suite stays green on older SDK installs. * Add `filterwarnings("ignore::FutureWarning")` to each new tool-factory test (and the parametrized metadata test) so they remain stable under strict warning configurations \u2014 the global dedup in `_feature_stage._WARNED_FEATURES` makes `pytest.warns` brittle across ordered runs. * Use `monkeypatch.setattr(..., None, raising=False)` instead of `delattr` in the missing-SDK-class test so it works for modules that implement PEP 562 `__getattr__`. * Split the long `get_bing_custom_search_tool` return into two lines for readability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry): harden tool-factory kwargs against silent override * Reorder the dict-literal kwargs assembly in get_azure_ai_search_tool, get_memory_search_tool, and get_bing_custom_search_tool so explicit parameters always take precedence over **kwargs (matching the safe pattern already used in get_a2a_tool). This prevents a caller passing `project_connection_id`, `index_name`, `memory_store_name`, `scope`, or `instance_name` through `**kwargs` from silently overriding the explicit security-sensitive arguments. * Update the README experimental note to reflect once-per-feature-id dedup semantics of `_feature_stage._WARNED_FEATURES` rather than claiming a per-factory "first use" warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(foundry): split FOUNDRY_TOOLS / FOUNDRY_PREVIEW_TOOLS, add bing-grounding - Add ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS to distinguish wrappers around preview Foundry SDK tool classes (Sharepoint/Fabric/Memory/ComputerUse/ BrowserAutomation/BingCustomSearch/A2A) from FOUNDRY_TOOLS, which is for GA-SDK wrappers that are simply new in agent-framework-foundry (AzureAISearch, BingGrounding). - Add get_bing_grounding_tool factory and a 'Choosing a web grounding tool' comparison block on get_web_search_tool / get_bing_grounding_tool / get_bing_custom_search_tool docstrings. - Drop the _require_sdk_class lazy resolver: every guarded class is available at azure-ai-projects>=2.1.0 (the package floor), so import them eagerly. Concrete return types replace 'Any'. - README: split the experimental factories into two tables, one per feature flag, with a note explaining the distinction. - Tests: split into FOUNDRY_TOOLS / FOUNDRY_PREVIEW_TOOLS factory cases; drop the obsolete missing-SDK-class ImportError test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../core/agent_framework/_feature_stage.py | 2 + python/packages/foundry/README.md | 67 +++ .../agent_framework_foundry/_chat_client.py | 403 +++++++++++++++++- .../tests/foundry/test_foundry_chat_client.py | 237 ++++++++++ 4 files changed, 699 insertions(+), 10 deletions(-) diff --git a/python/packages/core/agent_framework/_feature_stage.py b/python/packages/core/agent_framework/_feature_stage.py index 90235b0232..afcfc11267 100644 --- a/python/packages/core/agent_framework/_feature_stage.py +++ b/python/packages/core/agent_framework/_feature_stage.py @@ -49,6 +49,8 @@ class ExperimentalFeature(str, Enum): EVALS = "EVALS" FILE_HISTORY = "FILE_HISTORY" FIDES = "FIDES" + FOUNDRY_TOOLS = "FOUNDRY_TOOLS" + FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS" FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS" HARNESS = "HARNESS" SKILLS = "SKILLS" diff --git a/python/packages/foundry/README.md b/python/packages/foundry/README.md index 188535cd17..bbc139a1ae 100644 --- a/python/packages/foundry/README.md +++ b/python/packages/foundry/README.md @@ -39,3 +39,70 @@ async with Agent( result = await agent.run("What tools are available?") print(result.text) ``` + +## Hosted tool factories + +`FoundryChatClient` exposes static factory methods that return Foundry SDK tool +configurations ready to pass to an `Agent`'s `tools=[...]` argument. These +factories don't require a `FoundryChatClient` instance — you can call them +statically and reuse the same tool configuration across agents. + +```python +from agent_framework import Agent +from agent_framework.foundry import FoundryChatClient + +agent = Agent( + client=FoundryChatClient(...), + instructions="...", + tools=[ + FoundryChatClient.get_web_search_tool(), + FoundryChatClient.get_code_interpreter_tool(), + ], +) +``` + +Generally available factories: `get_code_interpreter_tool`, +`get_file_search_tool`, `get_web_search_tool`, +`get_image_generation_tool`, `get_mcp_tool`. + +> **Choosing a web grounding tool.** `get_web_search_tool` is the recommended +> default — it requires no separate Bing resource and works with Azure OpenAI +> models out of the box. Reach for `get_bing_grounding_tool` (experimental, +> see below) when you need finer Bing parameters (`count`, `freshness`, +> `market`, `set_lang`), are grounding non-OpenAI Foundry models, or are +> migrating from Grounding with Bing Search on the classic platform — it +> requires a Grounding with Bing Search Azure resource that you manage. +> `get_bing_custom_search_tool` (also experimental) is for grounding +> restricted to a curated list of domains via a Bing Custom Search instance. +> See the +> [web grounding overview](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview) +> for the full comparison. + +> **Experimental — `ExperimentalFeature.FOUNDRY_TOOLS`.** The following +> factories wrap GA Foundry tool SDK classes but are new wrappers in +> `agent-framework-foundry` and may change before the wrappers themselves +> reach GA. Calls emit an `ExperimentalWarning` the first time the +> `FOUNDRY_TOOLS` feature is exercised in a process (then deduplicated). + +| Factory | Foundry SDK tool | +|---------|-----------------| +| `get_azure_ai_search_tool(index_connection_id, index_name, ...)` | `AzureAISearchTool` | +| `get_bing_grounding_tool(connection_id, ...)` | `BingGroundingTool` | + +> **Experimental — `ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS`.** The +> following factories wrap **preview** Foundry tool SDK types — the underlying +> Foundry capability itself is in preview and may change or be removed before +> reaching GA. Calls emit a separate `ExperimentalWarning` the first time the +> `FOUNDRY_PREVIEW_TOOLS` feature is exercised in a process (then +> deduplicated). Use `FOUNDRY_TOOLS` for "wrapper is new" and +> `FOUNDRY_PREVIEW_TOOLS` for "underlying Foundry feature is preview". + +| Factory | Foundry SDK tool | +|---------|-----------------| +| `get_sharepoint_tool(connection_id)` | `SharepointPreviewTool` | +| `get_fabric_tool(connection_id)` | `MicrosoftFabricPreviewTool` | +| `get_memory_search_tool(memory_store_name, scope, ...)` | `MemorySearchPreviewTool` | +| `get_computer_use_tool(environment, display_width, display_height)` | `ComputerUsePreviewTool` | +| `get_browser_automation_tool(connection_id)` | `BrowserAutomationPreviewTool` | +| `get_bing_custom_search_tool(connection_id, instance_name, ...)` | `BingCustomSearchPreviewTool` | +| `get_a2a_tool(base_url=..., project_connection_id=..., ...)` | `A2APreviewTool` | diff --git a/python/packages/foundry/agent_framework_foundry/_chat_client.py b/python/packages/foundry/agent_framework_foundry/_chat_client.py index 7f8e033036..6d7dc878ff 100644 --- a/python/packages/foundry/agent_framework_foundry/_chat_client.py +++ b/python/packages/foundry/agent_framework_foundry/_chat_client.py @@ -16,14 +16,35 @@ from agent_framework import ( load_settings, ) from agent_framework._compaction import CompactionStrategy, TokenizerProtocol +from agent_framework._feature_stage import ExperimentalFeature, experimental from agent_framework._telemetry import get_user_agent from agent_framework.observability import ChatTelemetryLayer from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( + A2APreviewTool, + AISearchIndexResource, AutoCodeInterpreterToolParam, + AzureAISearchTool, + AzureAISearchToolResource, + BingCustomSearchConfiguration, + BingCustomSearchPreviewTool, + BingCustomSearchToolParameters, + BingGroundingSearchConfiguration, + BingGroundingSearchToolParameters, + BingGroundingTool, + BrowserAutomationPreviewTool, + BrowserAutomationToolConnectionParameters, + BrowserAutomationToolParameters, CodeInterpreterTool, + ComputerUsePreviewTool, + FabricDataAgentToolParameters, ImageGenTool, + MemorySearchPreviewTool, + MicrosoftFabricPreviewTool, + SharepointGroundingToolParameters, + SharepointPreviewTool, + ToolProjectConnection, WebSearchApproximateLocation, WebSearchTool, WebSearchToolFilters, @@ -381,17 +402,44 @@ class RawFoundryChatClient( # type: ignore[misc] custom_search_configuration: dict[str, Any] | None = None, **kwargs: Any, ) -> WebSearchTool: - """Create a web search tool configuration for Microsoft Foundry. + """Create a Web Search tool configuration for Microsoft Foundry. + + **Choosing a web grounding tool.** Foundry exposes three options that all reach + the public web via Bing. Pick the one that matches your scenario: + + * :py:meth:`get_web_search_tool` (this one, GA) — recommended starting point. + The Bing resource is managed by Microsoft, no extra Azure setup is required, + and only Azure OpenAI models are supported. Parameters are limited to + ``user_location`` and ``search_context_size``. + * :py:meth:`get_bing_grounding_tool` (preview) — use when you need finer Bing parameters (``count``, + ``freshness``, ``market``, ``set_lang``), want to ground non-OpenAI + Foundry models, or are migrating from Grounding with Bing Search on the + classic agents platform. You manage the Grounding with Bing Search + resource yourself (Contributor/Owner to create the resource, Foundry + Project Manager to wire the connection). + * :py:meth:`get_bing_custom_search_tool` (preview) — use when you need to + restrict grounding to a curated set of domains defined in a Bing Custom + Search instance. + + For all three, search data flows outside the Azure compliance boundary. See + https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview for + the full comparison. Keyword Args: - user_location: Location context with keys like "city", "country", "region", "timezone". - search_context_size: Amount of context from search results ("low", "medium", "high"). - allowed_domains: List of domains to restrict search results to. - custom_search_configuration: Custom Bing search configuration. - **kwargs: Additional arguments passed to the SDK WebSearchTool constructor. + user_location: Location context with keys like ``"city"``, ``"country"``, + ``"region"``, ``"timezone"``. + search_context_size: Amount of context from search results + (``"low"``, ``"medium"``, ``"high"``). + allowed_domains: List of domains to restrict search results to. Wrapped + into ``WebSearchToolFilters`` and passed as the ``filters`` field on + the SDK ``WebSearchTool``. + custom_search_configuration: Custom Bing search configuration for + domain-restricted scenarios. + **kwargs: Additional arguments passed to the SDK ``WebSearchTool`` + constructor. Returns: - A WebSearchTool ready to pass to an Agent. + A ``WebSearchTool`` ready to pass to an Agent. """ ws_kwargs: dict[str, Any] = {**kwargs} if search_context_size: @@ -400,15 +448,137 @@ class RawFoundryChatClient( # type: ignore[misc] ws_kwargs["filters"] = WebSearchToolFilters(allowed_domains=allowed_domains) if custom_search_configuration: ws_kwargs["custom_search_configuration"] = custom_search_configuration - ws_tool = WebSearchTool(**ws_kwargs) if user_location: - ws_tool.user_location = WebSearchApproximateLocation( + ws_kwargs["user_location"] = WebSearchApproximateLocation( city=user_location.get("city"), country=user_location.get("country"), region=user_location.get("region"), timezone=user_location.get("timezone"), ) - return ws_tool + return WebSearchTool(**ws_kwargs) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS) + def get_bing_grounding_tool( + *, + connection_id: str, + market: str | None = None, + set_lang: str | None = None, + count: int | None = None, + freshness: str | None = None, + **kwargs: Any, + ) -> BingGroundingTool: + """Create a Grounding with Bing Search tool configuration for Foundry. + + Use this factory when :py:meth:`get_web_search_tool` is too restrictive — for + example when you need ``count``/``freshness``/``market``/``set_lang`` + parameters, want to ground a non-OpenAI Foundry model, or are migrating an + agent that already uses Grounding with Bing Search on the classic agents + platform. You manage the Grounding with Bing Search Azure resource yourself + (Contributor or Owner to create the resource, Foundry Project Manager to + create the project connection). Search data flows outside the Azure + compliance boundary. + + For domain-restricted grounding to a curated allow-list, use + :py:meth:`get_bing_custom_search_tool` instead. For a zero-setup default that + works for most agents, see :py:meth:`get_web_search_tool`. The full + comparison lives at + https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview. + + Keyword Args: + connection_id: The Foundry project connection ID for the Grounding with + Bing Search resource. + market: Optional Bing market identifier (e.g. ``"en-US"``). + set_lang: Optional UI language code passed to the Bing API. + count: Optional number of search results to return. + freshness: Optional time-range filter for search results. See + https://learn.microsoft.com/bing/search-apis/bing-web-search/reference/query-parameters + for accepted values. + **kwargs: Additional arguments forwarded to the SDK + ``BingGroundingSearchConfiguration``. + + Returns: + A ``BingGroundingTool`` ready to pass to an Agent. + """ + config_kwargs: dict[str, Any] = { + **kwargs, + "project_connection_id": connection_id, + } + if market is not None: + config_kwargs["market"] = market + if set_lang is not None: + config_kwargs["set_lang"] = set_lang + if count is not None: + config_kwargs["count"] = count + if freshness is not None: + config_kwargs["freshness"] = freshness + return BingGroundingTool( + bing_grounding=BingGroundingSearchToolParameters( + search_configurations=[BingGroundingSearchConfiguration(**config_kwargs)], + ), + ) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS) + def get_bing_custom_search_tool( + *, + connection_id: str, + instance_name: str, + market: str | None = None, + set_lang: str | None = None, + count: int | None = None, + freshness: str | None = None, + **kwargs: Any, + ) -> BingCustomSearchPreviewTool: + """Create a Grounding with Bing Custom Search tool configuration for Foundry. + + Use this factory (preview) when you need to restrict grounding to a curated + list of domains. The allow/block list is defined ahead of time on a Bing + Custom Search resource (in the Bing portal) and referenced here by + ``instance_name``. Like the other Bing-backed tools, search data flows + outside the Azure compliance boundary, and you must create the Bing Custom + Search resource yourself. + + For unrestricted public-web grounding with no extra Azure setup, prefer + :py:meth:`get_web_search_tool`. For unrestricted grounding with finer Bing + parameters or non-OpenAI models, prefer :py:meth:`get_bing_grounding_tool`. + See + https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview + for the full comparison. + + Keyword Args: + connection_id: The Foundry project connection ID for the Grounding with + Bing Custom Search resource. + instance_name: The custom configuration instance name defined on the + Bing Custom Search resource. + market: Optional Bing market identifier (e.g. ``"en-US"``). + set_lang: Optional UI language code passed to the Bing API. + count: Optional number of search results to return. + freshness: Optional time-range filter for search results. + **kwargs: Additional arguments forwarded to the SDK + ``BingCustomSearchConfiguration``. + + Returns: + A ``BingCustomSearchPreviewTool`` ready to pass to an Agent. + """ + config_kwargs: dict[str, Any] = { + **kwargs, + "project_connection_id": connection_id, + "instance_name": instance_name, + } + if market is not None: + config_kwargs["market"] = market + if set_lang is not None: + config_kwargs["set_lang"] = set_lang + if count is not None: + config_kwargs["count"] = count + if freshness is not None: + config_kwargs["freshness"] = freshness + return BingCustomSearchPreviewTool( + bing_custom_search_preview=BingCustomSearchToolParameters( + search_configurations=[BingCustomSearchConfiguration(**config_kwargs)], + ), + ) @staticmethod def get_image_generation_tool( # type: ignore[override] @@ -513,6 +683,219 @@ class RawFoundryChatClient( # type: ignore[misc] # endregion + # region Experimental Foundry tool factories (preview SDK types) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS) + def get_azure_ai_search_tool( + *, + index_connection_id: str, + index_name: str, + query_type: str | None = None, + top_k: int | None = None, + filter: str | None = None, + index_asset_id: str | None = None, + **kwargs: Any, + ) -> AzureAISearchTool: + """Create an Azure AI Search tool configuration for Foundry. + + Keyword Args: + index_connection_id: The Foundry project connection ID for the Azure AI Search index. + index_name: The name of the index to search. + query_type: Optional query type (``"simple"``, ``"semantic"``, ``"vector"``, + ``"vector_simple_hybrid"``, or ``"vector_semantic_hybrid"``). + top_k: Optional number of documents to retrieve. + filter: Optional OData filter expression. + index_asset_id: Optional index asset id for the search resource. + **kwargs: Additional arguments forwarded to the SDK ``AISearchIndexResource``. + + Returns: + An ``AzureAISearchTool`` ready to pass to an Agent. + """ + index_kwargs: dict[str, Any] = { + **kwargs, + "project_connection_id": index_connection_id, + "index_name": index_name, + } + if query_type is not None: + index_kwargs["query_type"] = query_type + if top_k is not None: + index_kwargs["top_k"] = top_k + if filter is not None: + index_kwargs["filter"] = filter + if index_asset_id is not None: + index_kwargs["index_asset_id"] = index_asset_id + return AzureAISearchTool( + azure_ai_search=AzureAISearchToolResource(indexes=[AISearchIndexResource(**index_kwargs)]), + ) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS) + def get_sharepoint_tool( + *, + connection_id: str, + **kwargs: Any, + ) -> SharepointPreviewTool: + """Create a SharePoint grounding tool configuration for Foundry. + + Keyword Args: + connection_id: The Foundry project connection ID for the SharePoint resource. + **kwargs: Additional arguments forwarded to the SDK + ``SharepointGroundingToolParameters``. + + Returns: + A ``SharepointPreviewTool`` ready to pass to an Agent. + """ + return SharepointPreviewTool( + sharepoint_grounding_preview=SharepointGroundingToolParameters( + project_connections=[ToolProjectConnection(project_connection_id=connection_id)], + **kwargs, + ) + ) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS) + def get_fabric_tool( + *, + connection_id: str, + **kwargs: Any, + ) -> MicrosoftFabricPreviewTool: + """Create a Microsoft Fabric data agent tool configuration for Foundry. + + Keyword Args: + connection_id: The Foundry project connection ID for the Fabric data agent. + **kwargs: Additional arguments forwarded to the SDK + ``FabricDataAgentToolParameters``. + + Returns: + A ``MicrosoftFabricPreviewTool`` ready to pass to an Agent. + """ + return MicrosoftFabricPreviewTool( + fabric_dataagent_preview=FabricDataAgentToolParameters( + project_connections=[ToolProjectConnection(project_connection_id=connection_id)], + **kwargs, + ) + ) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS) + def get_memory_search_tool( + *, + memory_store_name: str, + scope: str, + search_options: Any | None = None, + update_delay: int | None = None, + **kwargs: Any, + ) -> MemorySearchPreviewTool: + """Create a Memory Search tool configuration for Foundry. + + Keyword Args: + memory_store_name: The name of the memory store to use. + scope: The namespace used to group and isolate memories (e.g. a user ID). + Use ``"{{$userId}}"`` to scope memories to the current signed-in user. + search_options: Optional ``MemorySearchOptions`` instance. + update_delay: Optional seconds to wait before updating memories after inactivity. + **kwargs: Additional arguments forwarded to the SDK ``MemorySearchPreviewTool``. + + Returns: + A ``MemorySearchPreviewTool`` ready to pass to an Agent. + """ + params: dict[str, Any] = { + **kwargs, + "memory_store_name": memory_store_name, + "scope": scope, + } + if search_options is not None: + params["search_options"] = search_options + if update_delay is not None: + params["update_delay"] = update_delay + return MemorySearchPreviewTool(**params) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS) + def get_computer_use_tool( + *, + environment: str, + display_width: int, + display_height: int, + **kwargs: Any, + ) -> ComputerUsePreviewTool: + """Create a Computer Use tool configuration for Foundry. + + Keyword Args: + environment: The computer environment to control. One of ``"windows"``, + ``"mac"``, ``"linux"``, ``"ubuntu"``, or ``"browser"``. + display_width: The width of the computer display. + display_height: The height of the computer display. + **kwargs: Additional arguments forwarded to the SDK ``ComputerUsePreviewTool``. + + Returns: + A ``ComputerUsePreviewTool`` ready to pass to an Agent. + """ + return ComputerUsePreviewTool( + environment=environment, + display_width=display_width, + display_height=display_height, + **kwargs, + ) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS) + def get_browser_automation_tool( + *, + connection_id: str, + **kwargs: Any, + ) -> BrowserAutomationPreviewTool: + """Create a Browser Automation tool configuration for Foundry. + + Keyword Args: + connection_id: The Foundry project connection ID for the Azure Playwright resource. + **kwargs: Additional arguments forwarded to the SDK + ``BrowserAutomationToolParameters``. + + Returns: + A ``BrowserAutomationPreviewTool`` ready to pass to an Agent. + """ + return BrowserAutomationPreviewTool( + browser_automation_preview=BrowserAutomationToolParameters( + connection=BrowserAutomationToolConnectionParameters(project_connection_id=connection_id), + **kwargs, + ) + ) + + @staticmethod + @experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS) + def get_a2a_tool( + *, + base_url: str | None = None, + agent_card_path: str | None = None, + project_connection_id: str | None = None, + **kwargs: Any, + ) -> A2APreviewTool: + """Create an Agent-to-Agent (A2A) tool configuration for Foundry. + + Keyword Args: + base_url: Base URL of the remote A2A agent. + agent_card_path: Path to the agent card relative to ``base_url``. + Defaults to ``"/.well-known/agent-card.json"`` server-side. + project_connection_id: Foundry connection ID for the A2A server. Stores + authentication and other connection details. + **kwargs: Additional arguments forwarded to the SDK ``A2APreviewTool``. + + Returns: + An ``A2APreviewTool`` ready to pass to an Agent. + """ + params: dict[str, Any] = dict(kwargs) + if base_url is not None: + params["base_url"] = base_url + if agent_card_path is not None: + params["agent_card_path"] = agent_card_path + if project_connection_id is not None: + params["project_connection_id"] = project_connection_id + return A2APreviewTool(**params) + + # endregion + class FoundryChatClient( # type: ignore[misc] FunctionInvocationLayer[FoundryChatOptionsT], diff --git a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py index 8f069b7f6d..5f0e34bc13 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py @@ -5,6 +5,7 @@ from __future__ import annotations import inspect import os import sys +import warnings from functools import wraps from pathlib import Path from typing import Annotated, Any @@ -984,6 +985,25 @@ def test_get_web_search_tool_with_location() -> None: assert tool_obj is not None +def test_get_web_search_tool_allowed_domains() -> None: + """allowed_domains is wrapped into the SDK filters field.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + tool_obj = RawFoundryChatClient.get_web_search_tool(allowed_domains=["example.com"]) + assert tool_obj.filters is not None + assert tool_obj.filters.allowed_domains == ["example.com"] + + +def test_get_web_search_tool_custom_search_configuration() -> None: + """custom_search_configuration is forwarded to the SDK without warning.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + tool_obj = RawFoundryChatClient.get_web_search_tool( + custom_search_configuration={"connection_id": "c", "instance_name": "i"}, + ) + assert tool_obj.custom_search_configuration == {"connection_id": "c", "instance_name": "i"} + + def test_get_image_generation_tool() -> None: """Test image generation tool creation.""" @@ -1012,6 +1032,223 @@ def test_get_mcp_tool_with_connection_id() -> None: assert tool_obj is not None +def _skip_if_sdk_class_missing(name: str) -> Any: + """Return the SDK class or skip the test if older azure-ai-projects lacks it.""" + from azure.ai.projects import models as projects_models + + cls = getattr(projects_models, name, None) + if cls is None: + pytest.skip(f"azure-ai-projects in this environment does not expose {name!r}.") + return cls + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_azure_ai_search_tool() -> None: + """Azure AI Search tool factory builds the nested resource correctly.""" + azure_ai_search_tool_cls = _skip_if_sdk_class_missing("AzureAISearchTool") + + tool_obj = FoundryChatClient.get_azure_ai_search_tool( + index_connection_id="conn-1", + index_name="my-index", + query_type="vector_semantic_hybrid", + top_k=5, + filter="category eq 'docs'", + ) + assert isinstance(tool_obj, azure_ai_search_tool_cls) + indexes = tool_obj.azure_ai_search.indexes + assert len(indexes) == 1 + index = indexes[0] + assert index.project_connection_id == "conn-1" + assert index.index_name == "my-index" + assert index.query_type == "vector_semantic_hybrid" + assert index.top_k == 5 + assert index.filter == "category eq 'docs'" + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_sharepoint_tool() -> None: + """SharePoint tool factory wires the connection through nested params.""" + sharepoint_tool_cls = _skip_if_sdk_class_missing("SharepointPreviewTool") + + tool_obj = FoundryChatClient.get_sharepoint_tool(connection_id="sp-conn") + assert isinstance(tool_obj, sharepoint_tool_cls) + connections = tool_obj.sharepoint_grounding_preview.project_connections + assert connections is not None + assert len(connections) == 1 + assert connections[0].project_connection_id == "sp-conn" + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_fabric_tool() -> None: + """Fabric tool factory wires the connection through nested params.""" + fabric_tool_cls = _skip_if_sdk_class_missing("MicrosoftFabricPreviewTool") + + tool_obj = FoundryChatClient.get_fabric_tool(connection_id="fab-conn") + assert isinstance(tool_obj, fabric_tool_cls) + connections = tool_obj.fabric_dataagent_preview.project_connections + assert connections is not None + assert len(connections) == 1 + assert connections[0].project_connection_id == "fab-conn" + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_memory_search_tool() -> None: + """Memory search tool factory passes core fields through.""" + memory_tool_cls = _skip_if_sdk_class_missing("MemorySearchPreviewTool") + + tool_obj = FoundryChatClient.get_memory_search_tool( + memory_store_name="store-1", + scope="{{$userId}}", + update_delay=600, + ) + assert isinstance(tool_obj, memory_tool_cls) + assert tool_obj.memory_store_name == "store-1" + assert tool_obj.scope == "{{$userId}}" + assert tool_obj.update_delay == 600 + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_computer_use_tool() -> None: + """Computer use tool factory passes environment + display dimensions.""" + computer_use_cls = _skip_if_sdk_class_missing("ComputerUsePreviewTool") + + tool_obj = FoundryChatClient.get_computer_use_tool( + environment="browser", + display_width=1920, + display_height=1080, + ) + assert isinstance(tool_obj, computer_use_cls) + assert tool_obj.environment == "browser" + assert tool_obj.display_width == 1920 + assert tool_obj.display_height == 1080 + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_browser_automation_tool() -> None: + """Browser automation tool factory wraps the connection id in the params type.""" + browser_tool_cls = _skip_if_sdk_class_missing("BrowserAutomationPreviewTool") + + tool_obj = FoundryChatClient.get_browser_automation_tool(connection_id="playwright-conn") + assert isinstance(tool_obj, browser_tool_cls) + assert tool_obj.browser_automation_preview.connection.project_connection_id == "playwright-conn" + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_bing_custom_search_tool() -> None: + """Bing custom search tool factory builds the nested search configuration.""" + bing_tool_cls = _skip_if_sdk_class_missing("BingCustomSearchPreviewTool") + + tool_obj = FoundryChatClient.get_bing_custom_search_tool( + connection_id="bing-conn", + instance_name="my-custom-config", + market="en-US", + count=10, + ) + assert isinstance(tool_obj, bing_tool_cls) + configs = tool_obj.bing_custom_search_preview.search_configurations + assert len(configs) == 1 + config = configs[0] + assert config.project_connection_id == "bing-conn" + assert config.instance_name == "my-custom-config" + assert config.market == "en-US" + assert config.count == 10 + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_bing_grounding_tool() -> None: + """Bing grounding tool factory builds the nested search configuration.""" + bing_tool_cls = _skip_if_sdk_class_missing("BingGroundingTool") + + tool_obj = FoundryChatClient.get_bing_grounding_tool( + connection_id="bing-conn", + market="en-US", + set_lang="en", + count=10, + freshness="Day", + ) + assert isinstance(tool_obj, bing_tool_cls) + configs = tool_obj.bing_grounding.search_configurations + assert len(configs) == 1 + config = configs[0] + assert config.project_connection_id == "bing-conn" + assert config.market == "en-US" + assert config.set_lang == "en" + assert config.count == 10 + assert config.freshness == "Day" + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +def test_get_a2a_tool() -> None: + """A2A tool factory carries base_url, agent_card_path, and project_connection_id.""" + a2a_tool_cls = _skip_if_sdk_class_missing("A2APreviewTool") + + tool_obj = FoundryChatClient.get_a2a_tool( + base_url="https://agent.example.com", + agent_card_path="/.well-known/agent-card.json", + project_connection_id="a2a-conn", + ) + assert isinstance(tool_obj, a2a_tool_cls) + assert tool_obj.base_url == "https://agent.example.com" + assert tool_obj.agent_card_path == "/.well-known/agent-card.json" + assert tool_obj.project_connection_id == "a2a-conn" + + +_FOUNDRY_TOOLS_FACTORY_CASES: list[tuple[str, str, dict[str, Any]]] = [ + ("get_azure_ai_search_tool", "AzureAISearchTool", {"index_connection_id": "c", "index_name": "i"}), + ( + "get_bing_grounding_tool", + "BingGroundingTool", + {"connection_id": "c"}, + ), +] + +_FOUNDRY_PREVIEW_TOOLS_FACTORY_CASES: list[tuple[str, str, dict[str, Any]]] = [ + ("get_sharepoint_tool", "SharepointPreviewTool", {"connection_id": "c"}), + ("get_fabric_tool", "MicrosoftFabricPreviewTool", {"connection_id": "c"}), + ( + "get_memory_search_tool", + "MemorySearchPreviewTool", + {"memory_store_name": "s", "scope": "u"}, + ), + ( + "get_computer_use_tool", + "ComputerUsePreviewTool", + {"environment": "browser", "display_width": 1, "display_height": 1}, + ), + ("get_browser_automation_tool", "BrowserAutomationPreviewTool", {"connection_id": "c"}), + ( + "get_bing_custom_search_tool", + "BingCustomSearchPreviewTool", + {"connection_id": "c", "instance_name": "i"}, + ), + ("get_a2a_tool", "A2APreviewTool", {"base_url": "https://a.example.com"}), +] + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +@pytest.mark.parametrize("factory_name, sdk_class_name, kwargs", _FOUNDRY_TOOLS_FACTORY_CASES) +def test_foundry_tools_factories_are_marked(factory_name: str, sdk_class_name: str, kwargs: dict[str, Any]) -> None: + """Factories wrapping GA Foundry tool SDK classes carry FOUNDRY_TOOLS metadata.""" + _skip_if_sdk_class_missing(sdk_class_name) + factory = getattr(FoundryChatClient, factory_name) + assert getattr(factory, "__feature_stage__", None) == "experimental" + assert getattr(factory, "__feature_id__", None) == "FOUNDRY_TOOLS" + assert factory(**kwargs) is not None + + +@pytest.mark.filterwarnings("ignore::FutureWarning") +@pytest.mark.parametrize("factory_name, sdk_class_name, kwargs", _FOUNDRY_PREVIEW_TOOLS_FACTORY_CASES) +def test_foundry_preview_tools_factories_are_marked( + factory_name: str, sdk_class_name: str, kwargs: dict[str, Any] +) -> None: + """Factories wrapping preview Foundry tool SDK classes carry FOUNDRY_PREVIEW_TOOLS metadata.""" + _skip_if_sdk_class_missing(sdk_class_name) + factory = getattr(FoundryChatClient, factory_name) + assert getattr(factory, "__feature_stage__", None) == "experimental" + assert getattr(factory, "__feature_id__", None) == "FOUNDRY_PREVIEW_TOOLS" + assert factory(**kwargs) is not None + + def test_parse_chunk_surfaces_oauth_consent_request() -> None: """An oauth_consent_request output item surfaces as Content with consent_link.""" From a12cc3878ee07eccb90a4afecfcc758c75af95d0 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Thu, 21 May 2026 11:05:58 +0100 Subject: [PATCH 18/22] .NET: Promote FoundryChatClient to public, add file/vector-store helpers and ToPromptAgentAsync converter (#5940) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Consolidate Foundry chat client decorators into FoundryChatClient - Replace AzureAIProjectChatClient and AzureAIProjectResponsesChatClient with a single internal sealed FoundryChatClient that covers three modes (pure responses, server-side agent reference, hosted agent endpoint). - Rename AzureAIProjectChatClientExtensions to AIProjectClientExtensions to reflect that it extends AIProjectClient. - All four AsAIAgent extension overloads and both FoundryAgent constructors now construct FoundryChatClient internally so the microsoft.foundry telemetry tag is uniform across paths. - Introduce AgentFrameworkUserAgentPolicy that stamps agent-framework-dotnet/{version} on outbound requests, mirroring the Python agent-framework-python/{version} contract. - Delete the Foundry-local MeaiUserAgentPolicy duplicate; rely on MEAI 10.5.1 to stamp MEAI/{version} automatically. - HostedAgentUserAgentPolicy keeps the combined foundry-hosting/agent-framework-dotnet/{version} segment (Python parity) and upgrades the bare segment in place to avoid duplication. - Tests reorganized: FoundryChatClientTests, AIProjectClientExtensionsTests, AgentFrameworkUserAgentPolicyTests, MeaiAutoUserAgentVerificationTests, plus in-place upgrade unit tests in HostedOutboundUserAgentTests. * Promote FoundryChatClient to public; add file/vector-store helpers and ToPromptAgentAsync converter - Promote FoundryChatClient from internal sealed to public sealed for Python parity, so .NET developers can hold and pass a FoundryChatClient directly the way Python developers do. - Mode 3 (hosted agent endpoint) now materializes an AIProjectClient from the parsed project root, making GetService() non-null across all three construction modes. This eliminates the per-mode asymmetry that previously hid project-level helpers from agents constructed via an agent endpoint URL. - Add four new instance methods on FoundryChatClient mirroring Python's spec: UploadFileAsync, DeleteFileAsync, CreateVectorStoreAsync (bundles upload + create + wait), DeleteVectorStoreAsync. Single overload each, path-only inputs to start; additional overloads can be added later without breaking callers. All are Experimental, consistent with the rest of the Foundry package. - Add ToPromptAgentAsync extension methods on ChatClientAgent and FoundryAgent for the agent-to-prompt-agent converter described in the Foundry spec. Mode 1 (responses API) synthesizes a DeclarativeAgentDefinition from the agent's ChatOptions; mode 2 (server-side agent reference, version, or record) returns the cached or freshly fetched Definition; mode 3 throws InvalidOperationException because no local definition exists to convert. - Strict AITool to ResponseTool mapping for mode 1: AIFunction becomes CreateFunctionTool with the function's JSON schema; AITool instances that wrap a ResponseTool unwrap via GetService(typeof(ResponseTool)); anything else throws InvalidOperationException naming the offending tool type. Matches the Python spec's unsupported-tools-raise-ValueError contract. - New unit tests: FoundryChatClientVectorStoreTests (22 tests covering all four helpers across the three FoundryChatClient construction modes plus validation and cancellation), FoundryPromptAgentConverterTests (16 tests covering both extension entry points across mode 1 synthesis, mode 2 cached and fetched paths, all failure modes, and a Python-parity guard asserting both extensions produce equivalent definitions for equivalent inputs), plus four new tests in FoundryChatClientTests for the mode 3 AIProjectClient materialization. * Stop building duplicate ProjectOpenAIClient in FoundryAgent agent-endpoint ctor After Plan #2's mode-3 AIProjectClient materialization, the inner FoundryChatClient already exposes a project-level AIProjectClient (via GetService) that internally provides the project-level ProjectOpenAIClient via GetProjectOpenAIClient(). FoundryAgent's agent-endpoint constructor was still independently constructing a second project-level ProjectOpenAIClient via the now-redundant CreateProjectLevelOpenAIClientFromAgentEndpoint helper — two handles to the same logical resource. Refactor: the agent-endpoint constructor now reads the inner FoundryChatClient's materialized AIProjectClient via base.GetService(typeof(AIProjectClient)) and derives the project-level ProjectOpenAIClient from it. The dead helper on both FoundryAgent (private static wrapper) and FoundryChatClient (the actual implementation) is removed. The user-supplied per-agent ClientPipelineOptions primitives (Transport, RetryPolicy, NetworkTimeout, UserAgentApplicationId) are propagated into the materialized AIProjectClientOptions so test-injected transports and explicit retry / timeout / user-agent settings reach the project-level pipeline — preserving the behavior the dead helper used to provide. Updated AgentEndpointConstructor_GetServiceAIProjectClient_ReturnsNull to its now-correct counterpart AgentEndpointConstructor_GetServiceAIProjectClient_ReturnsNonNull, since after Plan #2 the agent-endpoint ctor surfaces a non-null AIProjectClient (per user direction in Plan #2 Q2). * Strip duplicated AIProjectClient/ProjectOpenAIClient state from FoundryAgent Both _aiProjectClient and _projectOpenAIClient fields on FoundryAgent were redundant: - _aiProjectClient: FoundryAgent's GetService override returned this field, but DelegatingAIAgent.GetService → ChatClientAgent.GetService → FoundryChatClient.GetService already returns the same instance through the delegating chain. Field + override are pure duplication. - _projectOpenAIClient: only used by FoundryAgent's own GetService override and by CreateConversationSessionAsync. Per user direction, ProjectOpenAIClient is no longer exposed via GetService on either FoundryChatClient or FoundryAgent — callers retrieve it from the AIProjectClient themselves (aiProjectClient.GetProjectOpenAIClient()) the same way the framework does internally. This eliminates the mode-3 asymmetry where the chat client's stored ProjectOpenAIClient was per-agent (URL /agents/{name}/endpoint/protocols/openai) while the agent's was project-level. Refactor: - Delete both fields on FoundryAgent and the GetService override. - Delete the ProjectOpenAIClient branch from FoundryChatClient.GetService. - CreateConversationSessionAsync now resolves AIProjectClient at call time via this.GetService() and derives the conversations client from it. - Update FoundryChatClient tests that asserted on GetService to assert Null (deliberate removal). - Update FoundryAgent tests AgentEndpointConstructor_GetServiceProjectOpenAIClient_ReturnsNonNull and ProjectEndpointConstructor_GetServiceProjectOpenAIClient_ReturnsNonNull to ...ReturnsNull, and rewrite AgentEndpointConstructor_PropagatesUserAgentApplicationId_ToProjectLevelClient to look up AIProjectClient instead. No production code (only tests) referenced GetService, so this is a safe surface reduction. Net: 30 insertions, 61 deletions; FoundryAgent shrinks to a pure delegator with only the two convenience methods (CreateSessionAsync, CreateConversationSessionAsync) on top of the delegating chain. * Rename FoundryChatClient.HostedAgentName to AgentName and populate it for mode 2 The previous name implied a mode 3 only property tied to the hosted-agent endpoint URL. Today only hosted endpoints surface this name, but conceptually an agent name exists for every server-side agent the client talks to. Renaming to AgentName makes the property general-purpose and ready for future modes where the same chat client may target other server-side agent shapes that are not necessarily 'hosted'. Mode 2 (server-side agent reference) now mirrors AgentReference.Name into AgentName so callers have a uniform handle regardless of construction mode: * Mode 1 (pure responses): AgentName is null. There is no agent. * Mode 2 (AgentReference): AgentName == AgentReference.Name. * Mode 3 (agent endpoint URL): AgentName is parsed from the URL segment as before. Converter discriminator update: FoundryPromptAgentConverter previously used 'HostedAgentName is not null' to detect mode 3 and reject it. Now that mode 2 also populates AgentName, the mode 3 guard moves to the end of the resolution chain and uses the unambiguous 'AgentName is set AND no AgentReference exists' test. The user-visible error message and behavior are preserved. Dead-state cleanup spotted during format verify: * IDE0052 surfaced that FoundryChatClient._projectOpenAIClient is never read since the prior refactor stopped exposing ProjectOpenAIClient via GetService and rewired CreateConversationSessionAsync to resolve the AIProjectClient through the delegating chain. The field is deleted and its three ctor assignments removed. * HostedAgentEndpointInner.PerAgentClient only existed to plumb the per-agent ProjectOpenAIClient into that now-deleted field, so the property and its ctor parameter are removed. The local 'perAgentClient' variable inside BuildHostedAgentEndpointInner is still needed to derive the inner IChatClient, but no longer escapes the helper. Tests: * Mode1_PureResponses_ReturnsNullForAgentSpecificServices now also asserts AgentName is null. * New Mode2_AgentReference_PopulatesAgentNameFromAgentReference asserts the mode 2 mirror. * Mode3_HostedAgentEndpoint_ParsesAgentNameFromUrl renamed assertion target HostedAgentName to AgentName. Verification: 335/335 net10.0, 273/273 net472 Foundry unit; 229/229 Foundry.Hosting unit; format-verify (WSL2 + Docker mcr.microsoft.com/dotnet/sdk:10.0) clean on Microsoft.Agents.AI.Foundry. * Adopt canonical mode names: Responses Agent, Prompt Agent, Agent Endpoint Three FoundryChatClient construction modes now have one canonical noun used everywhere. * Responses Agent (Mode 1): inline ChatClientAgent, project-level Responses API, no server-side def. * Prompt Agent (Mode 2): server-side ProjectsAgentDefinition invoked by AgentReference. * Agent Endpoint (Mode 3): per-agent URL /agents/{name}/endpoint/protocols/openai. Hosted-or-not. 'Hosted' stays the kind of agent (Microsoft.Agents.AI.Foundry.Hosting). Not synonym of Mode 3. Rings: 1. XML docs + error messages use canonical names. en-GB to en-US: centralises, synthesise. 2. HostedAgentEndpointInner -> AgentEndpointInner, BuildHostedAgentEndpointInner -> BuildAgentEndpointInner. 3. Tests: Mode1_PureResponses_* -> Mode1_ResponsesAgent_*, Mode2_AgentReference_* -> Mode2_PromptAgent_*, Mode3_HostedAgentEndpoint_* -> Mode3_AgentEndpoint_*. Pure rename. No behavior change. 335/335 net10 + 273/273 net472 unit, format clean. * Address PR #5940 design feedback (Q-A through Q-F) Q-A: poll vector store til status leaves InProgress before return. Exp backoff 250ms-2s. Honor cancel. Q-B: try/catch upload loop. Mid-fail = best-effort DeleteFileAsync on already-uploaded ids. Swallow cleanup errors. Q-C: pinned AgentReference.Version uses GetAgentVersionAsync. Empty/whitespace/'latest' = GetLatest path. Q-D: HostedAgentUserAgentPolicy detects existing combined 'foundry-hosting/...' segment. No double prefix. Q-E: mode-3 vector-store test uses fake transport. No DNS to example.com. Q-F: no shim. Class always [Experimental] (since 8015e00f5, before dotnet-1.0.0). No compat contract. Callers rename to AIProjectClientExtensions. Rebase onto origin/main reconciliation: aad20c2b3 added public AsAIAgent(this AIProjectClient, Uri agentEndpoint, ...) extension that calls an internal FoundryAgent(AIProjectClient, Uri, ...) ctor. Reintroduced that ctor + a new FoundryChatClient(AIProjectClient, Uri, ProjectOpenAIClientOptions?) overload that reuses the supplied AIProjectClient's pipeline (via GetProjectResponsesClientForAgentEndpoint) instead of stamping a fresh credential. Verified: 346/346 net10 + 284/284 net472 Foundry unit, 230/230 Foundry.Hosting unit, format clean. * Add FoundryAgent helper extensions: UploadFile/DeleteFile/CreateVectorStore/DeleteVectorStore 4 thin forwarders on FoundryAgent that route to the inner FoundryChatClient's helpers via agent.GetService().X(). Live in existing FoundryAgentExtensions.cs alongside ToPromptAgentAsync. Throws InvalidOperationException when agent does not expose a FoundryChatClient via GetService (same pattern as ToPromptAgentAsync). Unit tests: FoundryAgentExtensionsTests covers all 4 forwarders + null-agent ArgumentNullException for each. 8 new tests, 354/354 net10 + 292/292 net472. Integration tests: parallel FoundryAgentExtensionsTests under Foundry.IntegrationTests mirrors the existing CreateAgent_CreatesAgentWithVectorStoresAsync shape (upload -> create vector store -> FileSearch tool answers question -> cleanup), but routes every helper call through the new FoundryAgent extensions. 4 new IT tests, all verified pass live against the real Foundry project (12-30s each). Skipped by default like the existing vector-store IT. * Address Sergey's PR review comments #1 (FoundryAgent.cs:139): drop unused aiProjectClient param from internal FoundryAgent(AIProjectClient, ChatClientAgent) ctor. Was discarded after null-check. Inner FoundryChatClient already surfaces AIProjectClient via GetService. 3 call sites in AIProjectClientExtensions updated. #2 (FoundryChatClient.cs:376): add pollingTimeout param to CreateVectorStoreAsync. Defaults to 5 min, configurable, Timeout.InfiniteTimeSpan disables. Throws TimeoutException with vector store id and elapsed seconds when bound exceeded. CancellationToken still wins. New unit test PollingTimeout_ThrowsTimeoutExceptionAsync. FoundryAgentExtensions forwarder updated to plumb the new param. Verified: 355/355 net10 + 293/293 net472 Foundry unit, 230/230 Foundry.Hosting unit, format clean. --- .../HostedAgentUserAgentPolicy.cs | 65 +- ...nsions.cs => AIProjectClientExtensions.cs} | 19 +- .../AgentFrameworkUserAgentPolicy.cs | 88 +++ .../AzureAIProjectChatClient.cs | 165 ----- .../AzureAIProjectResponsesChatClient.cs | 35 - .../ChatClientAgentFoundryExtensions.cs | 42 ++ .../FoundryAgent.cs | 235 ++----- .../FoundryAgentExtensions.cs | 116 +++ .../FoundryChatClient.cs | 647 +++++++++++++++++ .../FoundryPromptAgentConverter.cs | 150 ++++ .../RequestOptionsExtensions.cs | 58 -- .../FoundryAgentExtensionsTests.cs | 229 ++++++ ...yVersionedAgentStructuredOutputRunTests.cs | 4 +- .../HostedOutboundUserAgentTests.cs | 195 +++++- ...s.cs => AIProjectClientExtensionsTests.cs} | 115 ++- .../AgentFrameworkUserAgentPolicyTests.cs | 199 ++++++ .../AzureAIProjectChatClientTests.cs | 209 ------ .../FoundryAgentExtensionsTests.cs | 200 ++++++ .../FoundryAgentTests.cs | 156 +++-- .../FoundryChatClientTests.cs | 616 ++++++++++++++++ .../FoundryChatClientVectorStoreTests.cs | 660 ++++++++++++++++++ .../FoundryPromptAgentConverterTests.cs | 433 ++++++++++++ .../MeaiAutoUserAgentVerificationTests.cs | 90 +++ .../RequestOptionsExtensionsTests.cs | 115 --- 24 files changed, 3995 insertions(+), 846 deletions(-) rename dotnet/src/Microsoft.Agents.AI.Foundry/{AzureAIProjectChatClientExtensions.cs => AIProjectClientExtensions.cs} (96%) create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/AgentFrameworkUserAgentPolicy.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/ChatClientAgentFoundryExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgentExtensions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/FoundryChatClient.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/FoundryPromptAgentConverter.cs delete mode 100644 dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs create mode 100644 dotnet/tests/Foundry.IntegrationTests/FoundryAgentExtensionsTests.cs rename dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/{AzureAIProjectChatClientExtensionsTests.cs => AIProjectClientExtensionsTests.cs} (93%) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AgentFrameworkUserAgentPolicyTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentExtensionsTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientVectorStoreTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryPromptAgentConverterTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/MeaiAutoUserAgentVerificationTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs index e5e773db87..37f5970d4b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/HostedAgentUserAgentPolicy.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.ClientModel.Primitives; using System.Collections.Generic; using System.Reflection; @@ -9,8 +10,11 @@ using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Foundry.Hosting; /// -/// Pipeline policy that appends the hosted-agent User-Agent segment -/// (e.g. "foundry-hosting/agent-framework-dotnet/{version}") to outgoing requests. +/// Pipeline policy that emits the hosted-agent User-Agent segment +/// ("foundry-hosting/agent-framework-dotnet/{version}"), matching Python's hosted +/// contract (foundry-hosting/agent-framework-python/{version}, see +/// python/packages/core/agent_framework/_telemetry.py: the hosted prefix is joined +/// with the base agent-framework segment into a single combined User-Agent value). /// /// /// @@ -19,6 +23,12 @@ namespace Microsoft.Agents.AI.Foundry.Hosting; /// is already present in the User-Agent header, the policy does not append it again. /// /// +/// When a bare agent-framework-dotnet/{version} segment is already present (stamped by +/// the framework-wide AgentFrameworkUserAgentPolicy registered by +/// FoundryChatClient), this policy replaces that segment with the combined +/// hosted form so the wire never carries both forms simultaneously, preserving Python parity. +/// +/// /// This policy is added at hosted-agent resolution time via the MEAI 10.5.1 /// hook on the agent's underlying chat client. It is only /// registered when an agent is resolved by the Foundry hosting layer. @@ -30,6 +40,12 @@ internal sealed class HostedAgentUserAgentPolicy : PipelinePolicy private static readonly string s_supplementValue = CreateSupplementValue(); + /// Bare segment stamped by AgentFrameworkUserAgentPolicy in the non-hosted scenario; this policy upgrades it in-place when both run. + private const string BareAgentFrameworkPrefix = "agent-framework-dotnet/"; + + /// Combined hosted segment that this policy emits. Recognized in-place so callers whose pipelines already carry a (possibly different-version) combined segment get it replaced rather than double-prefixed (Q-D fix). + private const string CombinedHostedPrefix = "foundry-hosting/agent-framework-dotnet/"; + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) { AppendHeader(message); @@ -46,13 +62,52 @@ internal sealed class HostedAgentUserAgentPolicy : PipelinePolicy { if (message.Request.Headers.TryGetValue("User-Agent", out var existing) && !string.IsNullOrEmpty(existing)) { - // Guard against double-append on retries or when the policy - // is registered on multiple pipeline positions. - if (existing.Contains(s_supplementValue)) + // Guard against double-append on retries or when the policy is registered on + // multiple pipeline positions. + if (existing!.Contains(s_supplementValue)) { return; } + // Combined-form check first: if the caller's pipeline already has + // `foundry-hosting/agent-framework-dotnet/{version}` (with a version that differs + // from ours — otherwise the .Contains above would have returned early), replace the + // entire combined span in place. Without this, the bare-prefix search below would + // match `agent-framework-dotnet/` *inside* the combined segment and produce a + // malformed `foundry-hosting/foundry-hosting/agent-framework-dotnet/...` value. + var combinedIdx = existing.IndexOf(CombinedHostedPrefix, StringComparison.Ordinal); + if (combinedIdx >= 0) + { + var combinedEnd = existing.IndexOf(' ', combinedIdx); + if (combinedEnd < 0) + { + combinedEnd = existing.Length; + } + + var replacedCombined = string.Concat(existing.AsSpan(0, combinedIdx), s_supplementValue.AsSpan(), existing.AsSpan(combinedEnd)); + message.Request.Headers.Set("User-Agent", replacedCombined); + return; + } + + // If the bare agent-framework segment is present (stamped by + // AgentFrameworkUserAgentPolicy when not hosted), upgrade it in place to the + // combined hosted form so the wire never carries both segments simultaneously. + // Mirrors Python where get_user_agent() returns a single combined string when the + // hosted prefix is registered. + var idx = existing.IndexOf(BareAgentFrameworkPrefix, StringComparison.Ordinal); + if (idx >= 0) + { + var end = existing.IndexOf(' ', idx); + if (end < 0) + { + end = existing.Length; + } + + var replaced = string.Concat(existing.AsSpan(0, idx), s_supplementValue.AsSpan(), existing.AsSpan(end)); + message.Request.Headers.Set("User-Agent", replaced); + return; + } + message.Request.Headers.Set("User-Agent", $"{existing} {s_supplementValue}"); } else diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AIProjectClientExtensions.cs similarity index 96% rename from dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs rename to dotnet/src/Microsoft.Agents.AI.Foundry/AIProjectClientExtensions.cs index 8feb3bd465..d4b94a0f79 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AIProjectClientExtensions.cs @@ -23,7 +23,7 @@ namespace Azure.AI.Projects; /// Provides extension methods for . /// [Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] -public static partial class AzureAIProjectChatClientExtensions +public static partial class AIProjectClientExtensions { /// /// Uses an existing server side agent, wrapped as a using the provided and . @@ -63,7 +63,7 @@ public static partial class AzureAIProjectChatClientExtensions clientFactory, services); - return new FoundryAgent(aiProjectClient, innerAgent); + return new FoundryAgent(innerAgent); } /// @@ -132,7 +132,7 @@ public static partial class AzureAIProjectChatClientExtensions !allowDeclarativeMode, services); - return new FoundryAgent(aiProjectClient, innerAgent); + return new FoundryAgent(innerAgent); } /// @@ -165,7 +165,7 @@ public static partial class AzureAIProjectChatClientExtensions !allowDeclarativeMode, services); - return new FoundryAgent(aiProjectClient, innerAgent); + return new FoundryAgent(innerAgent); } /// @@ -246,7 +246,7 @@ public static partial class AzureAIProjectChatClientExtensions Func? clientFactory, IServiceProvider? services) { - IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentVersion, agentOptions.ChatOptions); + IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentVersion, agentOptions.ChatOptions); if (clientFactory is not null) { @@ -268,10 +268,7 @@ public static partial class AzureAIProjectChatClientExtensions Throw.IfNull(agentOptions.ChatOptions); Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); - IChatClient chatClient = aiProjectClient - .GetProjectOpenAIClient() - .GetResponsesClient() - .AsIChatClient(agentOptions.ChatOptions.ModelId); + IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentOptions.ChatOptions.ModelId); if (clientFactory is not null) { @@ -298,7 +295,7 @@ public static partial class AzureAIProjectChatClientExtensions Func? clientFactory, IServiceProvider? services) { - IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentRecord, agentOptions.ChatOptions); + IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentRecord, agentOptions.ChatOptions); if (clientFactory is not null) { @@ -316,7 +313,7 @@ public static partial class AzureAIProjectChatClientExtensions Func? clientFactory, IServiceProvider? services) { - IChatClient chatClient = new AzureAIProjectChatClient(aiProjectClient, agentReference, defaultModelId: null, agentOptions.ChatOptions); + IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentReference, defaultModelId: null, agentOptions.ChatOptions); if (clientFactory is not null) { diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AgentFrameworkUserAgentPolicy.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AgentFrameworkUserAgentPolicy.cs new file mode 100644 index 0000000000..5072e44c9c --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/AgentFrameworkUserAgentPolicy.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry; + +/// +/// Framework-wide pipeline policy that appends the agent-framework-dotnet/{version} +/// segment to outgoing User-Agent headers, mirroring the +/// agent-framework-python/{version} contract used by every Python provider package. +/// +/// +/// +/// The segment value is computed once from the Microsoft.Agents.AI.Foundry assembly's +/// . The policy is idempotent on retries: if +/// the segment is already present in the User-Agent header, the policy does not append +/// it again. +/// +/// +/// The policy is registered by FoundryChatClient on the underlying chat client's +/// OpenAIRequestPolicies hook so every outbound Foundry call carries the segment. The +/// policy is currently colocated with the Foundry package; it is expected to migrate to a +/// framework-wide location (such as Microsoft.Agents.AI) once another provider package +/// adopts the same User-Agent contract. +/// +/// +internal sealed class AgentFrameworkUserAgentPolicy : PipelinePolicy +{ + /// Gets the singleton policy instance. + public static AgentFrameworkUserAgentPolicy Instance { get; } = new AgentFrameworkUserAgentPolicy(); + + private static readonly string s_segmentValue = CreateSegmentValue(); + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + AppendHeader(message); + ProcessNext(message, pipeline, currentIndex); + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + AppendHeader(message); + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + + private static void AppendHeader(PipelineMessage message) + { + if (message.Request.Headers.TryGetValue("User-Agent", out var existing) && !string.IsNullOrEmpty(existing)) + { + // Guard against double-append on retries or when the policy + // is registered on multiple pipeline positions. + if (existing!.Contains(s_segmentValue)) + { + return; + } + + message.Request.Headers.Set("User-Agent", $"{existing} {s_segmentValue}"); + } + else + { + message.Request.Headers.Set("User-Agent", s_segmentValue); + } + } + + private static string CreateSegmentValue() + { + const string Name = "agent-framework-dotnet"; + + if (typeof(AgentFrameworkUserAgentPolicy).Assembly.GetCustomAttribute()?.InformationalVersion is string version) + { + int pos = version.IndexOf('+'); + if (pos >= 0) + { + version = version.Substring(0, pos); + } + + if (version.Length > 0) + { + return $"{Name}/{version}"; + } + } + + return Name; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs deleted file mode 100644 index c9a121bfc4..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectChatClient.cs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; -using Azure.AI.Projects.Agents; -using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; -using Microsoft.Shared.Diagnostics; -using OpenAI.Responses; - -namespace Microsoft.Agents.AI.Foundry; - -/// -/// Provides a chat client implementation that integrates with Azure AI Agents, enabling chat interactions using -/// Azure-specific agent capabilities. -/// -[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] -internal sealed class AzureAIProjectChatClient : DelegatingChatClient -{ - private readonly ChatClientMetadata? _metadata; - private readonly AIProjectClient _agentClient; - private readonly ProjectsAgentVersion? _agentVersion; - private readonly ProjectsAgentRecord? _agentRecord; - private readonly ChatOptions? _chatOptions; - private readonly AgentReference _agentReference; - - /// - /// Initializes a new instance of the class. - /// - /// An instance of to interact with Azure AI Agents services. - /// An instance of representing the specific agent to use. - /// The default model to use for the agent, if applicable. - /// An instance of representing the options on how the agent was predefined. - /// - /// The provided should be decorated with a for proper functionality. - /// - internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, AgentReference agentReference, string? defaultModelId, ChatOptions? chatOptions) - : base(Throw.IfNull(aiProjectClient) - .GetProjectOpenAIClient() - .GetProjectResponsesClientForAgent(agentReference) - .AsIChatClient()) - { - this._agentClient = aiProjectClient; - this._agentReference = Throw.IfNull(agentReference); - this._metadata = new ChatClientMetadata("microsoft.foundry", defaultModelId: defaultModelId); - this._chatOptions = chatOptions; - } - - /// - /// Initializes a new instance of the class. - /// - /// An instance of to interact with Azure AI Agents services. - /// An instance of representing the specific agent to use. - /// An instance of representing the options on how the agent was predefined. - /// - /// The provided should be decorated with a for proper functionality. - /// - internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, ProjectsAgentRecord agentRecord, ChatOptions? chatOptions) - : this(aiProjectClient, Throw.IfNull(agentRecord).GetLatestVersion(), chatOptions) - { - this._agentRecord = agentRecord; - } - - internal AzureAIProjectChatClient(AIProjectClient aiProjectClient, ProjectsAgentVersion agentVersion, ChatOptions? chatOptions) - : this( - aiProjectClient, - CreateAgentReference(Throw.IfNull(agentVersion)), - (agentVersion.Definition as DeclarativeAgentDefinition)?.Model, - chatOptions) - { - this._agentVersion = agentVersion; - } - - /// - /// Creates an from an . - /// Uses the agent version's version if available, otherwise defaults to "latest". - /// - /// The agent version to create a reference from. - /// An for the specified agent version. - private static AgentReference CreateAgentReference(ProjectsAgentVersion agentVersion) - { - // If the version is null, empty, or whitespace, use "latest" as the default. - // This handles cases where hosted agents (like MCP agents) may not have a version assigned. - var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; - return new AgentReference(agentVersion.Name, version); - } - - /// - public override object? GetService(Type serviceType, object? serviceKey = null) - { - return (serviceKey is null && serviceType == typeof(ChatClientMetadata)) - ? this._metadata - : (serviceKey is null && serviceType == typeof(AIProjectClient)) - ? this._agentClient - : (serviceKey is null && serviceType == typeof(ProjectsAgentVersion)) - ? this._agentVersion - : (serviceKey is null && serviceType == typeof(ProjectsAgentRecord)) - ? this._agentRecord - : (serviceKey is null && serviceType == typeof(AgentReference)) - ? this._agentReference - : base.GetService(serviceType, serviceKey); - } - - /// - public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - var agentOptions = this.GetAgentEnabledChatOptions(options); - - return await base.GetResponseAsync(messages, agentOptions, cancellationToken).ConfigureAwait(false); - } - - /// - public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var agentOptions = this.GetAgentEnabledChatOptions(options); - - await foreach (var chunk in base.GetStreamingResponseAsync(messages, agentOptions, cancellationToken).ConfigureAwait(false)) - { - yield return chunk; - } - } - - private ChatOptions GetAgentEnabledChatOptions(ChatOptions? options) - { - // Start with a clone of the base chat options defined for the agent, if any. - ChatOptions agentEnabledChatOptions = this._chatOptions?.Clone() ?? new(); - - // Ignore per-request all options that can't be overridden. - agentEnabledChatOptions.Instructions = null; - agentEnabledChatOptions.Tools = null; - agentEnabledChatOptions.Temperature = null; - agentEnabledChatOptions.TopP = null; - agentEnabledChatOptions.PresencePenalty = null; - agentEnabledChatOptions.ResponseFormat = null; - - // Use the conversation from the request, or the one defined at the client level. - agentEnabledChatOptions.ConversationId = options?.ConversationId ?? this._chatOptions?.ConversationId; - - // Preserve the original RawRepresentationFactory - var originalFactory = options?.RawRepresentationFactory; - - agentEnabledChatOptions.RawRepresentationFactory = (client) => - { - if (originalFactory?.Invoke(this) is not CreateResponseOptions responseCreationOptions) - { - responseCreationOptions = new CreateResponseOptions(); - } - - responseCreationOptions.Agent = this._agentReference; -#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - responseCreationOptions.Patch.Remove("$.model"u8); -#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - - return responseCreationOptions; - }; - - return agentEnabledChatOptions; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs deleted file mode 100644 index a768d102f8..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/AzureAIProjectResponsesChatClient.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Azure.AI.Projects; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI.Foundry; - -#pragma warning disable OPENAI001 -internal sealed class AzureAIProjectResponsesChatClient : DelegatingChatClient -{ - private readonly ChatClientMetadata _metadata; - private readonly AIProjectClient _aiProjectClient; - - internal AzureAIProjectResponsesChatClient(AIProjectClient aiProjectClient, string defaultModelId) - : base(Throw.IfNull(aiProjectClient) - .GetProjectOpenAIClient() - .GetProjectResponsesClientForModel(Throw.IfNullOrWhitespace(defaultModelId)) - .AsIChatClient()) - { - this._aiProjectClient = aiProjectClient; - this._metadata = new ChatClientMetadata("microsoft.foundry", defaultModelId: defaultModelId); - } - - public override object? GetService(Type serviceType, object? serviceKey = null) - { - return (serviceKey is null && serviceType == typeof(ChatClientMetadata)) - ? this._metadata - : (serviceKey is null && serviceType == typeof(AIProjectClient)) - ? this._aiProjectClient - : base.GetService(serviceType, serviceKey); - } -} -#pragma warning restore OPENAI001 diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/ChatClientAgentFoundryExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/ChatClientAgentFoundryExtensions.cs new file mode 100644 index 0000000000..772675108b --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/ChatClientAgentFoundryExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects.Agents; +using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Foundry; + +/// +/// Foundry-specific extensions on . Mirrors Python's free +/// to_prompt_agent(agent) function for agents whose underlying chat client is a +/// . +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +public static class ChatClientAgentFoundryExtensions +{ + /// + /// Converts the supplied agent into a ready to publish + /// via AgentAdministrationClient.CreateAgentVersionAsync. + /// + /// + /// Only works on agents whose chat client is a and whose + /// construction mode is convertible. The Agent Endpoint construction mode (Mode 3) is not + /// convertible because no local definition exists; conversion in that case throws. + /// + /// The chat client agent to convert. + /// A token that can cancel an internal server-side fetch when the agent was constructed from a bare . + /// A suitable for publishing. + /// is . + /// The agent's chat client is not a ; the agent was constructed via the Agent Endpoint mode (Mode 3); no model id is set on the agent's for the Responses Agent mode (Mode 1); or the agent contains an that cannot be converted to a ResponseTool. + public static Task ToPromptAgentAsync(this ChatClientAgent agent, CancellationToken cancellationToken = default) + { + Throw.IfNull(agent); + return FoundryPromptAgentConverter.ConvertAsync(agent.ChatClient, agent.GetService(), cancellationToken); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs index 30bfc84d9a..e412bb35b9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgent.cs @@ -39,11 +39,6 @@ namespace Microsoft.Agents.AI.Foundry; [Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] public sealed class FoundryAgent : DelegatingAIAgent { - /// - /// The cached supplied to or constructed by the active constructor. - /// - private readonly AIProjectClient _aiProjectClient; - /// /// Initializes a new instance of the class using the direct Responses API path. /// @@ -73,9 +68,8 @@ public sealed class FoundryAgent : DelegatingAIAgent : base(CreateInnerAgent( CreateProjectClient(projectEndpoint, credential, clientOptions), model, instructions, name, description, tools, clientFactory, loggerFactory, services, - out var aiProjectClient)) + out _)) { - this._aiProjectClient = aiProjectClient; } /// @@ -87,9 +81,11 @@ public sealed class FoundryAgent : DelegatingAIAgent /// /// The authentication credential. /// - /// Optional configuration for the underlying . When supplied: + /// Optional configuration for the underlying . When supplied: /// /// The instance is passed through to the per-agent client; pipeline policies added via AddPolicy(...) on it execute on the per-agent traffic. + /// Endpoint and are owned by this constructor and are overwritten with values derived from ; any caller value is replaced. + /// For the project-level conversations client a separate fresh options bag is built that copies only , , , and UserAgentApplicationId; pipeline policies added via AddPolicy(...) do not propagate to the conversations pipeline. /// /// /// Optional tools to use when interacting with the agent. @@ -113,43 +109,37 @@ public sealed class FoundryAgent : DelegatingAIAgent IList? tools = null, Func? clientFactory = null, IServiceProvider? services = null) - : base(CreateInnerAgentFromAgentEndpoint(agentEndpoint, credential, clientOptions, tools, clientFactory, services, out var aiProjectClient)) + : base(CreateInnerAgentFromAgentEndpoint(agentEndpoint, credential, clientOptions, tools, clientFactory, services)) { - this._aiProjectClient = aiProjectClient; } /// - /// Initializes a new instance of the class from an agent-specific - /// endpoint while reusing an existing . + /// Internal constructor used by the AsAIAgent(this AIProjectClient, Uri, ...) + /// extension where the caller already has an and the agent + /// endpoint URI. Reuses the supplied client's pipeline (no new credential or transport is + /// stamped) and surfaces the agent through a just like the + /// public agent-endpoint ctor. /// - /// An existing rooted at the same project as . - /// - /// The agent-specific endpoint URI. Must be of the shape - /// https://<host>/.../projects/<project>/agents/<agentName>/endpoint/protocols/openai. - /// - /// Optional tools to use when interacting with the agent. - /// Provides a way to customize the creation of the underlying . - /// Optional service provider for resolving dependencies required by AI functions. - /// or is null. - /// does not match the expected agent-endpoint shape. internal FoundryAgent( AIProjectClient aiProjectClient, Uri agentEndpoint, IList? tools = null, Func? clientFactory = null, IServiceProvider? services = null) - : base(BuildAgentEndpointInnerAgent(aiProjectClient, agentEndpoint, clientOptions: null, tools, clientFactory, services)) + : base(CreateInnerAgentFromAgentEndpointReusingProjectClient(aiProjectClient, agentEndpoint, tools, clientFactory, services)) { - this._aiProjectClient = Throw.IfNull(aiProjectClient); } /// - /// Internal constructor used by AsAIAgent extension methods that already have an and a configured . + /// Internal constructor used by AsAIAgent extension methods that already have a + /// configured . The inner agent already routes through a + /// whose GetService<AIProjectClient>() surfaces + /// the project client to downstream callers, so the agent does not also need a private + /// reference here. /// - internal FoundryAgent(AIProjectClient aiProjectClient, ChatClientAgent innerAgent) + internal FoundryAgent(ChatClientAgent innerAgent) : base(WireClientHeaders(Throw.IfNull(innerAgent))) { - this._aiProjectClient = Throw.IfNull(aiProjectClient); } #region Convenience methods @@ -182,7 +172,13 @@ public sealed class FoundryAgent : DelegatingAIAgent /// A linked to the newly created server-side conversation. public async Task CreateConversationSessionAsync(CancellationToken cancellationToken = default) { - var conversationsClient = this._aiProjectClient.ProjectOpenAIClient.GetProjectConversationsClient(); + // The inner FoundryChatClient surfaces an AIProjectClient via GetService for all + // three construction modes (Plan #2 Agent Endpoint mode materialization). Resolve it through the + // delegating chain at call time instead of caching a private reference on this agent. + var aiProjectClient = this.GetService() + ?? throw new InvalidOperationException( + "FoundryAgent inner chain does not expose an AIProjectClient; cannot create a project-level conversation session."); + var conversationsClient = aiProjectClient.GetProjectOpenAIClient().GetProjectConversationsClient(); var conversation = (await conversationsClient.CreateProjectConversationAsync(options: null, cancellationToken).ConfigureAwait(false)).Value; @@ -196,17 +192,6 @@ public sealed class FoundryAgent : DelegatingAIAgent #endregion - /// - public override object? GetService(Type serviceType, object? serviceKey = null) - { - if (serviceKey is null && serviceType == typeof(AIProjectClient)) - { - return this._aiProjectClient; - } - - return base.GetService(serviceType, serviceKey); - } - #region Private helpers private static AIAgent CreateInnerAgent( @@ -251,7 +236,7 @@ public sealed class FoundryAgent : DelegatingAIAgent Throw.IfNull(agentOptions.ChatOptions); Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId); - IChatClient chatClient = new AzureAIProjectResponsesChatClient(aiProjectClient, agentOptions.ChatOptions.ModelId); + IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentOptions.ChatOptions.ModelId); if (clientFactory is not null) { @@ -288,16 +273,10 @@ public sealed class FoundryAgent : DelegatingAIAgent } /// - /// Builds the inner for the agent-endpoint constructor by - /// constructing a project-scoped and using - /// . - /// This routes the outbound URL through the per-agent endpoint shape that the Foundry service - /// expects for hosted agents and lets the SDK auto-append the api-version query string. - /// Caller-supplied are passed through to the per-agent - /// client with Endpoint and - /// overridden by values derived from - /// ; any policies the caller added via AddPolicy - /// remain in effect on the per-agent pipeline. The MEAI user-agent policy is appended last. + /// Builds the inner for the agent-endpoint constructor. The + /// per-agent shape and URL parsing are owned by + /// ; we just construct it in the Agent Endpoint mode (Mode 3) + /// and pass the inner chat client through any caller-provided . /// private static AIAgent CreateInnerAgentFromAgentEndpoint( Uri agentEndpoint, @@ -305,44 +284,14 @@ public sealed class FoundryAgent : DelegatingAIAgent ProjectOpenAIClientOptions? clientOptions, IList? tools, Func? clientFactory, - IServiceProvider? services, - out AIProjectClient outClient) + IServiceProvider? services) { Throw.IfNull(agentEndpoint); Throw.IfNull(credential); - var (_, projectRoot) = ParseAgentEndpoint(agentEndpoint); - outClient = CreateProjectClient(projectRoot, credential, CreateProjectClientOptions(clientOptions)); + IChatClient chatClient = new FoundryChatClient(agentEndpoint, credential, clientOptions); + var agentName = ((FoundryChatClient)chatClient).AgentName!; - return BuildAgentEndpointInnerAgent(outClient, agentEndpoint, clientOptions, tools, clientFactory, services); - } - - /// - /// Builds the inner for an agent endpoint against a pre-built - /// . The caller is responsible for ensuring the supplied client - /// is rooted at the same project as ; the agent name is - /// parsed from the endpoint URI and passed to - /// . - /// - private static AIAgent BuildAgentEndpointInnerAgent( - AIProjectClient aiProjectClient, - Uri agentEndpoint, - ProjectOpenAIClientOptions? clientOptions, - IList? tools, - Func? clientFactory, - IServiceProvider? services) - { - Throw.IfNull(aiProjectClient); - Throw.IfNull(agentEndpoint); - - var (agentName, _) = ParseAgentEndpoint(agentEndpoint); - - var perAgentOptions = clientOptions ?? new ProjectOpenAIClientOptions(); - perAgentOptions.AddPolicy(RequestOptionsExtensions.UserAgentPolicy, PipelinePosition.PerCall); - - IChatClient chatClient = aiProjectClient.ProjectOpenAIClient - .GetProjectResponsesClientForAgentEndpoint(agentName, options: perAgentOptions) - .AsIChatClient(); if (clientFactory is not null) { chatClient = clientFactory(chatClient); @@ -358,6 +307,46 @@ public sealed class FoundryAgent : DelegatingAIAgent return WireClientHeaders(new ChatClientAgent(chatClient, agentOptions, services: services)); } + /// + /// Variant of that reuses an existing + /// 's pipeline instead of stamping a fresh credential. Used by + /// the AsAIAgent(AIProjectClient, Uri agentEndpoint, ...) extension overload. + /// + private static AIAgent CreateInnerAgentFromAgentEndpointReusingProjectClient( + AIProjectClient aiProjectClient, + Uri agentEndpoint, + IList? tools, + Func? clientFactory, + IServiceProvider? services) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(agentEndpoint); + + IChatClient chatClient = new FoundryChatClient(aiProjectClient, agentEndpoint, clientOptions: null); + var agentName = ((FoundryChatClient)chatClient).AgentName!; + + if (clientFactory is not null) + { + chatClient = clientFactory(chatClient); + } + + ChatClientAgentOptions agentOptions = new() + { + Id = agentName, + Name = agentName, + ChatOptions = new() { Tools = tools }, + }; + + return WireClientHeaders(new ChatClientAgent(chatClient, agentOptions, services: services)); + } + + /// + /// Parses an agent endpoint URI. Delegates to + /// so the chat client and the agent share a single source of truth for the URL shape. + /// + internal static (string AgentName, Uri ProjectRoot) ParseAgentEndpoint(Uri agentEndpoint) + => FoundryChatClient.ParseAgentEndpoint(agentEndpoint); + /// /// Parses an agent endpoint URI of shape /// https://<host>/.../projects/<project>/agents/<agentName>/endpoint/protocols/openai @@ -369,90 +358,12 @@ public sealed class FoundryAgent : DelegatingAIAgent /// strips query string and fragment. Throws for inputs that /// do not match the expected shape. /// - /// - /// The endpoint is missing the /agents/ segment, has an empty agent name, or has a - /// suffix other than /endpoint/protocols/openai. - /// - internal static (string AgentName, Uri ProjectRoot) ParseAgentEndpoint(Uri agentEndpoint) - { - Throw.IfNull(agentEndpoint); - - const string AgentsSegment = "/agents/"; - const string ExpectedSuffix = "/endpoint/protocols/openai"; - - var path = agentEndpoint.AbsolutePath.TrimEnd('/'); - var idx = path.IndexOf(AgentsSegment, StringComparison.OrdinalIgnoreCase); - if (idx < 0) - { - throw new ArgumentException( - $"Expected an agent endpoint of shape 'https:///.../projects//agents//endpoint/protocols/openai' but got '{agentEndpoint}'.", - nameof(agentEndpoint)); - } - - var afterAgents = path.Substring(idx + AgentsSegment.Length); - var nextSlash = afterAgents.IndexOf('/'); - if (nextSlash <= 0) - { - throw new ArgumentException( - $"Agent endpoint '{agentEndpoint}' is missing the '{ExpectedSuffix}' suffix.", - nameof(agentEndpoint)); - } - - var agentName = afterAgents.Substring(0, nextSlash); - var suffix = afterAgents.Substring(nextSlash); - if (!string.Equals(suffix, ExpectedSuffix, StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException( - $"Agent endpoint '{agentEndpoint}' has an unexpected suffix '{suffix}'. Expected '{ExpectedSuffix}'.", - nameof(agentEndpoint)); - } - - var rootPath = path.Substring(0, idx); - var projectRoot = new UriBuilder(agentEndpoint) - { - Path = rootPath, - Query = string.Empty, - Fragment = string.Empty, - }.Uri; - - return (agentName, projectRoot); - } - private static AIProjectClient CreateProjectClient(Uri endpoint, AuthenticationTokenProvider credential, AIProjectClientOptions? clientOptions = null) { Throw.IfNull(endpoint); Throw.IfNull(credential); - clientOptions ??= new AIProjectClientOptions(); - clientOptions.AddPolicy(RequestOptionsExtensions.UserAgentPolicy, PipelinePosition.PerCall); - return new AIProjectClient(endpoint, credential, clientOptions); - } - - internal static AIProjectClientOptions? CreateProjectClientOptions(ProjectOpenAIClientOptions? clientOptions) - { - if (clientOptions is null) - { - return null; - } - - // Copy pipeline behavior the caller configured on the per-agent options bag onto the - // project-level options bag so the agent endpoint client honors it. UserAgentApplicationId - // is project-level (not derived from the agent endpoint), so it must be carried through too. - var projectOptions = new AIProjectClientOptions - { - Transport = clientOptions.Transport, - RetryPolicy = clientOptions.RetryPolicy, - NetworkTimeout = clientOptions.NetworkTimeout, - MessageLoggingPolicy = clientOptions.MessageLoggingPolicy, - UserAgentApplicationId = clientOptions.UserAgentApplicationId, - }; - - if (clientOptions.ClientLoggingOptions is not null) - { - projectOptions.ClientLoggingOptions = clientOptions.ClientLoggingOptions; - } - - return projectOptions; + return new AIProjectClient(endpoint, credential, clientOptions ?? new AIProjectClientOptions()); } #endregion diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgentExtensions.cs new file mode 100644 index 0000000000..db079dc9ff --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryAgentExtensions.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects.Agents; +using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; +using OpenAI.Files; +using OpenAI.VectorStores; + +#pragma warning disable OPENAI001 + +namespace Microsoft.Agents.AI.Foundry; + +/// +/// Foundry-specific extensions on . Hosts the prompt-agent converter +/// plus thin forwarders that surface the file and vector-store helpers from the inner +/// at the agent level so callers do not need to drop down to +/// agent.GetService<FoundryChatClient>().X() for common workflows. +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +public static class FoundryAgentExtensions +{ + /// + /// Converts the supplied into a + /// ready to publish via AgentAdministrationClient.CreateAgentVersionAsync. + /// + /// + /// The Agent Endpoint construction mode (Mode 3) is not convertible because no local + /// definition exists; conversion in that case throws . + /// + /// The Foundry agent to convert. + /// A token that can cancel an internal server-side fetch when the agent was constructed from a bare . + /// A suitable for publishing. + /// is . + /// The agent's chat client is not a ; the agent was constructed via the Agent Endpoint mode (Mode 3); no model id is set on the agent's for the Responses Agent mode (Mode 1); or the agent contains an that cannot be converted to a ResponseTool. + public static Task ToPromptAgentAsync(this FoundryAgent agent, CancellationToken cancellationToken = default) + { + Throw.IfNull(agent); + + var innerChatClient = agent.GetService() + ?? throw new InvalidOperationException( + "ToPromptAgentAsync could not resolve the inner IChatClient on the FoundryAgent."); + var chatOptions = agent.GetService(); + return FoundryPromptAgentConverter.ConvertAsync(innerChatClient, chatOptions, cancellationToken); + } + + /// + /// Uploads a file to the project. Thin forwarder to + /// + /// on the agent's inner . + /// + /// The Foundry agent whose inner chat client owns the upload pipeline. + /// Path to the file to upload. + /// The upload purpose (e.g. ). + /// A token that can cancel the upload. + /// is . + /// The agent does not expose a via . + public static Task UploadFileAsync(this FoundryAgent agent, string filePath, FileUploadPurpose purpose, CancellationToken cancellationToken = default) + => RequireFoundryChatClient(agent).UploadFileAsync(filePath, purpose, cancellationToken); + + /// + /// Deletes a previously uploaded file. Thin forwarder to + /// . + /// + /// The Foundry agent whose inner chat client owns the file pipeline. + /// The file id returned by . + /// A token that can cancel the delete. + /// is . + /// The agent does not expose a . + public static Task DeleteFileAsync(this FoundryAgent agent, string fileId, CancellationToken cancellationToken = default) + => RequireFoundryChatClient(agent).DeleteFileAsync(fileId, cancellationToken); + + /// + /// Uploads the supplied files, creates a vector store containing them, and waits until the + /// store leaves the in-progress state. Thin forwarder to + /// . + /// + /// The Foundry agent whose inner chat client owns the file and vector-store pipeline. + /// The vector store name. + /// Paths to files to upload and attach to the store. + /// Optional last-active-at expiration window. + /// Optional upper bound on the wait for the vector store to leave the in-progress state. Defaults to 5 minutes; pass to disable. + /// A token that can cancel the orchestration. + /// is . + /// The agent does not expose a . + /// The vector store did not leave the in-progress state within . + public static Task CreateVectorStoreAsync(this FoundryAgent agent, string name, IEnumerable filePaths, TimeSpan? expiresAfter = null, TimeSpan? pollingTimeout = null, CancellationToken cancellationToken = default) + => RequireFoundryChatClient(agent).CreateVectorStoreAsync(name, filePaths, expiresAfter, pollingTimeout, cancellationToken); + + /// + /// Deletes a vector store. Thin forwarder to + /// . + /// + /// The Foundry agent whose inner chat client owns the vector-store pipeline. + /// The vector store id. + /// A token that can cancel the delete. + /// is . + /// The agent does not expose a . + public static Task DeleteVectorStoreAsync(this FoundryAgent agent, string vectorStoreId, CancellationToken cancellationToken = default) + => RequireFoundryChatClient(agent).DeleteVectorStoreAsync(vectorStoreId, cancellationToken); + + private static FoundryChatClient RequireFoundryChatClient(FoundryAgent agent) + { + Throw.IfNull(agent); + return agent.GetService() + ?? throw new InvalidOperationException( + "FoundryAgent does not expose a FoundryChatClient via GetService(). " + + "File and vector-store helpers require the agent's inner chat client to be a FoundryChatClient."); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryChatClient.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryChatClient.cs new file mode 100644 index 0000000000..6d7144af18 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryChatClient.cs @@ -0,0 +1,647 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; +using OpenAI.Files; +using OpenAI.Responses; +using OpenAI.VectorStores; + +#pragma warning disable OPENAI001 + +namespace Microsoft.Agents.AI.Foundry; + +/// +/// Foundry chat-client decorator that unifies the three Foundry chat-client construction +/// modes (Responses Agent, Prompt Agent, Agent Endpoint) behind a single type and centralizes +/// Foundry-specific concerns: microsoft.foundry telemetry tagging, +/// agent-framework-dotnet/{version} User-Agent stamping, and (for Prompt Agents) +/// per-request payload mutation that injects the agent reference and strips per-request +/// overrides that the server owns. +/// +/// +/// +/// Replaces the previous AzureAIProjectChatClient and AzureAIProjectResponsesChatClient +/// decorators. All Foundry entry points (the public FoundryAgent constructors and the +/// AIProjectClientExtensions.AsAIAgent overloads) now construct a +/// internally, so telemetry and the agent-framework User-Agent +/// segment are uniform across paths. +/// +/// +/// The three construction modes are: +/// +/// +/// Responses Agent (Mode 1): direct Responses API call against a project-level model id; no server-side agent definition exists. Constructed from (AIProjectClient, modelId). +/// Prompt Agent (Mode 2): server-side agent definition (a , typically a ) invoked by against the project Responses URL. Constructed from , , or . +/// Agent Endpoint (Mode 3): invocation via the per-agent endpoint URL …/projects/{p}/agents/{name}/endpoint/protocols/openai. The agent behind the endpoint can be either a hosted (container-backed) agent or a Prompt Agent. Constructed from (Uri agentEndpoint, credential). +/// +/// +/// Note: "Hosted Agent" refers to a container-based runtime agent (see +/// Microsoft.Agents.AI.Foundry.Hosting) and is the kind of agent that may sit +/// behind an Agent Endpoint. It is not synonymous with the Agent Endpoint mode itself. +/// +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +public sealed class FoundryChatClient : DelegatingChatClient +{ + private readonly ChatClientMetadata _metadata; + private readonly AIProjectClient? _aiProjectClient; + private readonly AgentReference? _agentReference; + private readonly ProjectsAgentVersion? _agentVersion; + private readonly ProjectsAgentRecord? _agentRecord; + private readonly ChatOptions? _baseChatOptions; + + /// + /// Initializes a new instance for the Responses Agent mode (Mode 1): direct Responses API + /// call against a project-level model id; no server-side agent definition exists. + /// + /// The project client. + /// The model deployment id. + internal FoundryChatClient(AIProjectClient aiProjectClient, string modelId) + : base(Throw.IfNull(aiProjectClient) + .GetProjectOpenAIClient() + .GetProjectResponsesClientForModel(Throw.IfNullOrWhitespace(modelId)) + .AsIChatClient()) + { + this._aiProjectClient = aiProjectClient; + this._metadata = new ChatClientMetadata("microsoft.foundry", defaultModelId: modelId); + TryRegisterAgentFrameworkUserAgentPolicy(this.InnerClient); + } + + /// + /// Initializes a new instance for the Prompt Agent mode (Mode 2): server-side agent + /// definition invoked by . + /// + internal FoundryChatClient(AIProjectClient aiProjectClient, AgentReference agentReference, string? defaultModelId, ChatOptions? baseChatOptions) + : base(Throw.IfNull(aiProjectClient) + .GetProjectOpenAIClient() + .GetProjectResponsesClientForAgent(Throw.IfNull(agentReference)) + .AsIChatClient()) + { + this._aiProjectClient = aiProjectClient; + this._agentReference = agentReference; + this._metadata = new ChatClientMetadata("microsoft.foundry", defaultModelId: defaultModelId); + this._baseChatOptions = baseChatOptions; + this.AgentName = agentReference.Name; + TryRegisterAgentFrameworkUserAgentPolicy(this.InnerClient); + } + + /// + /// Initializes a new instance for the Prompt Agent mode (Mode 2, record variant): + /// server-side agent definition invoked by record, resolving to the latest version. + /// + internal FoundryChatClient(AIProjectClient aiProjectClient, ProjectsAgentRecord agentRecord, ChatOptions? baseChatOptions) + : this(aiProjectClient, Throw.IfNull(agentRecord).GetLatestVersion(), baseChatOptions) + { + this._agentRecord = agentRecord; + } + + /// + /// Initializes a new instance for the Prompt Agent mode (Mode 2, version variant): + /// server-side agent definition invoked by a specific version. + /// + internal FoundryChatClient(AIProjectClient aiProjectClient, ProjectsAgentVersion agentVersion, ChatOptions? baseChatOptions) + : this( + aiProjectClient, + CreateAgentReference(Throw.IfNull(agentVersion)), + (agentVersion.Definition as DeclarativeAgentDefinition)?.Model, + baseChatOptions) + { + this._agentVersion = agentVersion; + } + + /// + /// Initializes a new instance for the Agent Endpoint mode (Mode 3): invocation via the + /// per-agent endpoint URL. Parses the URL into its per-agent + /// shape internally and forwards through the resulting + /// responses client. + /// + /// + /// The agent-specific endpoint URI. Must be of the shape + /// https://<host>/.../projects/<project>/agents/<agentName>/endpoint/protocols/openai. + /// + /// The authentication credential. + /// Optional per-agent client options. Endpoint and AgentName are owned by this ctor and overridden with values derived from . + internal FoundryChatClient(Uri agentEndpoint, AuthenticationTokenProvider credential, ProjectOpenAIClientOptions? clientOptions) + : this(BuildAgentEndpointInner(agentEndpoint, credential, clientOptions)) + { + } + + /// + /// Initializes a new instance for the Agent Endpoint mode (Mode 3) by reusing an existing + /// 's pipeline. Equivalent to the + /// + /// constructor but skips building a fresh per-agent pipeline: the project-level + /// on is used directly. + /// + /// The project client already configured at the project root containing . + /// The per-agent endpoint URI. Same shape constraints as the other agent-endpoint ctor. + /// Optional per-agent client options applied to the per-agent GetProjectResponsesClientForAgentEndpoint call. + internal FoundryChatClient(AIProjectClient aiProjectClient, Uri agentEndpoint, ProjectOpenAIClientOptions? clientOptions) + : this(BuildAgentEndpointInnerFromProjectClient(aiProjectClient, agentEndpoint, clientOptions)) + { + } + + private FoundryChatClient(AgentEndpointInner inner) + : base(inner.ChatClient) + { + this._aiProjectClient = inner.AIProjectClient; + this.AgentName = inner.AgentName; + this._metadata = new ChatClientMetadata("microsoft.foundry"); + TryRegisterAgentFrameworkUserAgentPolicy(this.InnerClient); + } + + /// + /// Gets the agent name associated with this chat client. + /// + /// + /// Set in two cases: + /// + /// + /// + /// Prompt Agent mode (Mode 2): the value of supplied at + /// construction. + /// + /// + /// + /// + /// Agent Endpoint mode (Mode 3): the agent name segment parsed from the supplied agent + /// endpoint URI. + /// + /// + /// + /// + /// Returns for the Responses Agent mode (Mode 1) where no agent name + /// exists. + /// + /// + internal string? AgentName { get; } + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + return (serviceKey is null && serviceType == typeof(ChatClientMetadata)) + ? this._metadata + : (serviceKey is null && serviceType == typeof(AIProjectClient)) + ? this._aiProjectClient + : (serviceKey is null && serviceType == typeof(AgentReference)) + ? this._agentReference + : (serviceKey is null && serviceType == typeof(ProjectsAgentVersion)) + ? this._agentVersion + : (serviceKey is null && serviceType == typeof(ProjectsAgentRecord)) + ? this._agentRecord + : base.GetService(serviceType, serviceKey); + } + + /// + public override async Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + var effectiveOptions = this._agentReference is not null + ? this.GetAgentEnabledChatOptions(options) + : options; + + return await base.GetResponseAsync(messages, effectiveOptions, cancellationToken).ConfigureAwait(false); + } + + /// + public override async IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var effectiveOptions = this._agentReference is not null + ? this.GetAgentEnabledChatOptions(options) + : options; + + await foreach (var chunk in base.GetStreamingResponseAsync(messages, effectiveOptions, cancellationToken).ConfigureAwait(false)) + { + yield return chunk; + } + } + + #region File and vector-store helpers (mirrors Python's foundry_chat_client surface) + + /// + /// Uploads a single file to the project for the supplied purpose. The upload is performed + /// against the project-level reachable via + /// , so this method works uniformly across all three + /// FoundryChatClient construction modes. + /// + /// Absolute or relative path to the file to upload. The file must exist. + /// The file upload purpose (e.g. ). + /// A token that can cancel the upload. + /// The created as returned by the service. + /// is . + /// The file at does not exist. + public async Task UploadFileAsync(string filePath, FileUploadPurpose purpose, CancellationToken cancellationToken = default) + { + Throw.IfNull(filePath); + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"File not found: '{filePath}'.", filePath); + } + + var fileClient = this.GetOpenAIFileClient(); + // Use the Stream overload to honor cancellation; the (string, purpose) overload has no + // CancellationToken parameter in the OpenAI SDK. + using var stream = File.OpenRead(filePath); + var result = await fileClient.UploadFileAsync(stream, Path.GetFileName(filePath), purpose, cancellationToken).ConfigureAwait(false); + return result.Value; + } + + /// Deletes a file previously uploaded to the project. + /// The file id returned by . + /// A token that can cancel the delete. + /// The deletion result. + /// is or whitespace. + public async Task DeleteFileAsync(string fileId, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhitespace(fileId); + var fileClient = this.GetOpenAIFileClient(); + var result = await fileClient.DeleteFileAsync(fileId, cancellationToken).ConfigureAwait(false); + return result.Value; + } + + /// + /// Uploads the supplied files, creates a vector store containing them, waits until the + /// store finishes ingesting its files (status leaves ), + /// and returns the . Mirrors Python's + /// foundry_chat_client.create_vector_store(name, files, expires_after_days). + /// + /// The vector store name. + /// Paths to files to upload and attach to the store. + /// Optional last-active-at expiration window. When supplied, the vector store expires this many days after its last use. + /// Optional upper bound on the wait for the vector store to leave . Defaults to 5 minutes when not supplied; pass to disable. Independent of : cancellation always wins. + /// A token that can cancel the orchestration. + /// The created and fully-ready . The returned instance reflects the state observed after polling completes; it may be in (typical), , or any other terminal status returned by the service. Only is polled. + /// + /// + /// File-upload semantics are best-effort: when one of the per-file uploads throws, this method + /// makes a best-effort attempt to delete the files it has already uploaded so they do not + /// accumulate as orphaned resources on the project, then rethrows the original exception. The + /// cleanup itself does not throw — its failures are silently ignored because the caller is + /// already receiving a more meaningful exception from the original upload failure. + /// + /// + /// Cancellation aborts the polling loop with an ; any + /// already-uploaded files and the partially-created vector store remain on the project and are + /// the caller's responsibility to clean up. The same applies when the polling timeout elapses + /// (a is thrown instead). + /// + /// + /// is or whitespace, or is . + /// The vector store did not leave within . + public async Task CreateVectorStoreAsync(string name, IEnumerable filePaths, TimeSpan? expiresAfter = null, TimeSpan? pollingTimeout = null, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhitespace(name); + Throw.IfNull(filePaths); + + var fileIds = new List(); + try + { + foreach (var path in filePaths) + { + cancellationToken.ThrowIfCancellationRequested(); + var uploaded = await this.UploadFileAsync(path, FileUploadPurpose.Assistants, cancellationToken).ConfigureAwait(false); + fileIds.Add(uploaded.Id); + } + } + catch + { + // Q-B: best-effort cleanup of files already uploaded before the mid-loop failure so + // they do not accumulate as orphaned resources on the project. Swallow cleanup + // exceptions — the caller is already going to see the original upload exception, and + // there is nothing useful we can do with a secondary delete failure. + await this.BestEffortDeleteFilesAsync(fileIds).ConfigureAwait(false); + throw; + } + + var options = new VectorStoreCreationOptions + { + Name = name, + }; + foreach (var id in fileIds) + { + options.FileIds.Add(id); + } + if (expiresAfter is { } window) + { + options.ExpirationPolicy = new VectorStoreExpirationPolicy(VectorStoreExpirationAnchor.LastActiveAt, (int)Math.Ceiling(window.TotalDays)); + } + + var vectorStoreClient = this.GetVectorStoreClient(); + var createResult = await vectorStoreClient.CreateVectorStoreAsync(options, cancellationToken).ConfigureAwait(false); + var created = createResult.Value; + + // Q-A: poll until the vector store leaves the in-progress state. Without this the helper + // hands the caller a vector store whose file ingestion may still be running, defeating + // the purpose of the one-call wrapper. + return await WaitForVectorStoreReadyAsync(vectorStoreClient, created, pollingTimeout ?? s_defaultPollingTimeout, cancellationToken).ConfigureAwait(false); + } + + private async Task BestEffortDeleteFilesAsync(IEnumerable fileIds) + { + foreach (var id in fileIds) + { + try + { + // Pass CancellationToken.None: cleanup runs in the catch path; the caller's + // token may already be cancelled and we still want to do our best to free + // orphaned resources before propagating the original exception. + await this.DeleteFileAsync(id, CancellationToken.None).ConfigureAwait(false); + } + catch + { + // Silently ignore cleanup failures; see XML doc on CreateVectorStoreAsync. + } + } + } + + /// Upper bound on when the caller does not supply one. Chosen to comfortably cover normal Foundry vector-store ingestion (seconds to a minute for modest file sets) while still surfacing a clear failure if the server is stuck. + private static readonly TimeSpan s_defaultPollingTimeout = TimeSpan.FromMinutes(5); + + private static async Task WaitForVectorStoreReadyAsync(VectorStoreClient client, VectorStore initial, TimeSpan timeout, CancellationToken cancellationToken) + { + if (initial.Status != VectorStoreStatus.InProgress) + { + return initial; + } + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var delay = TimeSpan.FromMilliseconds(250); + var maxDelay = TimeSpan.FromSeconds(2); + var current = initial; + while (current.Status == VectorStoreStatus.InProgress) + { + if (timeout != Timeout.InfiniteTimeSpan && stopwatch.Elapsed >= timeout) + { + throw new TimeoutException( + $"Vector store '{current.Id}' did not leave the in-progress state within {timeout.TotalSeconds:0.##} seconds."); + } + + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + var refreshed = await client.GetVectorStoreAsync(current.Id, cancellationToken).ConfigureAwait(false); + current = refreshed.Value; + + if (delay < maxDelay) + { + var next = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); + delay = next < maxDelay ? next : maxDelay; + } + } + + return current; + } + + /// Deletes a vector store. The associated files (if any) are not deleted by this method; call separately to clean them up. + /// The vector store id. + /// A token that can cancel the delete. + /// The deletion result. + /// is or whitespace. + public async Task DeleteVectorStoreAsync(string vectorStoreId, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhitespace(vectorStoreId); + var vectorStoreClient = this.GetVectorStoreClient(); + var result = await vectorStoreClient.DeleteVectorStoreAsync(vectorStoreId, cancellationToken).ConfigureAwait(false); + return result.Value; + } + + private OpenAIFileClient GetOpenAIFileClient() + { + var projectClient = this._aiProjectClient + ?? throw new InvalidOperationException("This FoundryChatClient does not have an AIProjectClient available. File and vector-store helpers require an AIProjectClient."); + return projectClient.GetProjectOpenAIClient().GetOpenAIFileClient(); + } + + private VectorStoreClient GetVectorStoreClient() + { + var projectClient = this._aiProjectClient + ?? throw new InvalidOperationException("This FoundryChatClient does not have an AIProjectClient available. File and vector-store helpers require an AIProjectClient."); + return projectClient.GetProjectOpenAIClient().GetVectorStoreClient(); + } + + #endregion + + /// + /// Parses an agent endpoint URI of shape + /// https://<host>/.../projects/<project>/agents/<agentName>/endpoint/protocols/openai + /// and returns the agent name and the derived project-root URI. + /// + /// + /// Tolerates trailing slash, casing variants on /agents/ and the suffix segment, and + /// strips query string and fragment. Throws for inputs that + /// do not match the expected shape. + /// + /// + /// The endpoint is missing the /agents/ segment, has an empty agent name, or has a + /// suffix other than /endpoint/protocols/openai. + /// + internal static (string AgentName, Uri ProjectRoot) ParseAgentEndpoint(Uri agentEndpoint) + { + Throw.IfNull(agentEndpoint); + + const string AgentsSegment = "/agents/"; + const string ExpectedSuffix = "/endpoint/protocols/openai"; + + var path = agentEndpoint.AbsolutePath.TrimEnd('/'); + var idx = path.IndexOf(AgentsSegment, StringComparison.OrdinalIgnoreCase); + if (idx < 0) + { + throw new ArgumentException( + $"Expected an agent endpoint of shape 'https:///.../projects//agents//endpoint/protocols/openai' but got '{agentEndpoint}'. " + + "If you want to construct a FoundryAgent against a project endpoint, use the (Uri projectEndpoint, AuthenticationTokenProvider credential, string model, string instructions, ...) constructor instead.", + nameof(agentEndpoint)); + } + + var afterAgents = path.Substring(idx + AgentsSegment.Length); + var nextSlash = afterAgents.IndexOf('/'); + if (nextSlash <= 0) + { + throw new ArgumentException( + $"Agent endpoint '{agentEndpoint}' is missing the '{ExpectedSuffix}' suffix.", + nameof(agentEndpoint)); + } + + var agentName = afterAgents.Substring(0, nextSlash); + var suffix = afterAgents.Substring(nextSlash); + if (!string.Equals(suffix, ExpectedSuffix, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + $"Agent endpoint '{agentEndpoint}' has an unexpected suffix '{suffix}'. Expected '{ExpectedSuffix}'.", + nameof(agentEndpoint)); + } + + var rootPath = path.Substring(0, idx); + var projectRoot = new UriBuilder(agentEndpoint) + { + Path = rootPath, + Query = string.Empty, + Fragment = string.Empty, + }.Uri; + + return (agentName, projectRoot); + } + + private ChatOptions GetAgentEnabledChatOptions(ChatOptions? options) + { + // Start with a clone of the base chat options defined for the agent, if any. + ChatOptions agentEnabledChatOptions = this._baseChatOptions?.Clone() ?? new(); + + // Ignore per-request all options that can't be overridden. + agentEnabledChatOptions.Instructions = null; + agentEnabledChatOptions.Tools = null; + agentEnabledChatOptions.Temperature = null; + agentEnabledChatOptions.TopP = null; + agentEnabledChatOptions.PresencePenalty = null; + agentEnabledChatOptions.ResponseFormat = null; + + // Use the conversation from the request, or the one defined at the client level. + agentEnabledChatOptions.ConversationId = options?.ConversationId ?? this._baseChatOptions?.ConversationId; + + // Preserve the original RawRepresentationFactory. + var originalFactory = options?.RawRepresentationFactory; + + agentEnabledChatOptions.RawRepresentationFactory = (client) => + { + if (originalFactory?.Invoke(this) is not CreateResponseOptions responseCreationOptions) + { + responseCreationOptions = new CreateResponseOptions(); + } + + responseCreationOptions.Agent = this._agentReference; +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + responseCreationOptions.Patch.Remove("$.model"u8); +#pragma warning restore SCME0001 + + return responseCreationOptions; + }; + + return agentEnabledChatOptions; + } + + private static AgentReference CreateAgentReference(ProjectsAgentVersion agentVersion) + { + // If the version is null, empty, or whitespace, use "latest" as the default. This handles + // cases where hosted agents (like MCP agents) may not have a version assigned. + var version = string.IsNullOrWhiteSpace(agentVersion.Version) ? "latest" : agentVersion.Version; + return new AgentReference(agentVersion.Name, version); + } + + private static AgentEndpointInner BuildAgentEndpointInner( + Uri agentEndpoint, + AuthenticationTokenProvider credential, + ProjectOpenAIClientOptions? clientOptions) + { + Throw.IfNull(agentEndpoint); + Throw.IfNull(credential); + + var (agentName, projectRoot) = ParseAgentEndpoint(agentEndpoint); + + var perAgentOptions = clientOptions ?? new ProjectOpenAIClientOptions(); + perAgentOptions.Endpoint = agentEndpoint; + perAgentOptions.AgentName = agentName; + + var authPolicy = new BearerTokenPolicy(credential, AzureAiResourceScope); + var perAgentClient = new ProjectOpenAIClient(authPolicy, perAgentOptions); + + var chatClient = perAgentClient.GetProjectResponsesClient().AsIChatClient(); + + // Materialize a project-level AIProjectClient from the parsed project root so + // GetService() returns non-null for all FoundryChatClient + // construction modes. Project-level helpers (file upload, vector store create/delete) + // depend on this. RBAC for those calls is at the project level; if the supplied + // credential lacks project-scope permissions, the SDK surfaces a clean 401/403 at + // call time. The four observable primitive ClientPipelineOptions properties are + // propagated from the caller's per-agent options bag so test-injected transports and + // explicit RetryPolicy / NetworkTimeout / UserAgentApplicationId reach the + // project-level pipeline. Pipeline policies added via AddPolicy on the caller bag are + // NOT propagated because ClientPipelineOptions does not publicly enumerate policies. + var aiProjectClientOptions = new AIProjectClientOptions(); + if (clientOptions is not null) + { + if (clientOptions.RetryPolicy is not null) + { + aiProjectClientOptions.RetryPolicy = clientOptions.RetryPolicy; + } + if (clientOptions.NetworkTimeout is not null) + { + aiProjectClientOptions.NetworkTimeout = clientOptions.NetworkTimeout; + } + if (clientOptions.Transport is not null) + { + aiProjectClientOptions.Transport = clientOptions.Transport; + } + if (!string.IsNullOrEmpty(clientOptions.UserAgentApplicationId)) + { + aiProjectClientOptions.UserAgentApplicationId = clientOptions.UserAgentApplicationId; + } + } + var aiProjectClient = new AIProjectClient(projectRoot, credential, aiProjectClientOptions); + + return new AgentEndpointInner(chatClient, aiProjectClient, agentName); + } + + private static AgentEndpointInner BuildAgentEndpointInnerFromProjectClient( + AIProjectClient aiProjectClient, + Uri agentEndpoint, + ProjectOpenAIClientOptions? clientOptions) + { + Throw.IfNull(aiProjectClient); + Throw.IfNull(agentEndpoint); + + var (agentName, _) = ParseAgentEndpoint(agentEndpoint); + + var perAgentOptions = clientOptions ?? new ProjectOpenAIClientOptions(); + perAgentOptions.Endpoint = agentEndpoint; + perAgentOptions.AgentName = agentName; + + var chatClient = aiProjectClient.GetProjectOpenAIClient() + .GetProjectResponsesClientForAgentEndpoint(agentName, options: perAgentOptions) + .AsIChatClient(); + + // Reuse the caller's AIProjectClient verbatim — no new pipeline is materialized. + return new AgentEndpointInner(chatClient, aiProjectClient, agentName); + } + + /// Best-effort registration of via the MEAI hook with at-most-once dedup per pipeline. + private static void TryRegisterAgentFrameworkUserAgentPolicy(IChatClient? innerClient) + { + if (innerClient?.GetService() is { } policies) + { + // OpenAIRequestPoliciesReflection.AddPolicyIfMissing performs a check-then-add against + // the private _entries collection on the OpenAIRequestPolicies instance, so the + // policy is registered at most once even when many FoundryChatClient instances share + // the same underlying chat client. + OpenAIRequestPoliciesReflection.AddPolicyIfMissing( + policies, + AgentFrameworkUserAgentPolicy.Instance, + PipelinePosition.PerCall); + } + } + + /// Default OAuth scope for the Azure AI resource. Matches the scope used by Azure.AI.Extensions.OpenAI's internal authentication helper so the bearer token is accepted by the Foundry control plane. + private const string AzureAiResourceScope = "https://ai.azure.com/.default"; + + private readonly struct AgentEndpointInner + { + public AgentEndpointInner(IChatClient chatClient, AIProjectClient aiProjectClient, string agentName) + { + this.ChatClient = chatClient; + this.AIProjectClient = aiProjectClient; + this.AgentName = agentName; + } + + public IChatClient ChatClient { get; } + public AIProjectClient AIProjectClient { get; } + public string AgentName { get; } + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryPromptAgentConverter.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryPromptAgentConverter.cs new file mode 100644 index 0000000000..5cf7f71580 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Foundry/FoundryPromptAgentConverter.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Microsoft.Extensions.AI; +using Microsoft.Shared.DiagnosticIds; +using Microsoft.Shared.Diagnostics; +using OpenAI.Responses; + +#pragma warning disable OPENAI001 + +namespace Microsoft.Agents.AI.Foundry; + +/// +/// Shared internal implementation behind the public ToPromptAgentAsync extension methods +/// on and . Converts a Foundry-backed +/// agent into a ready to publish via +/// . +/// +/// +/// +/// Dispatch by construction mode (reachable via +/// ): +/// +/// +/// Responses Agent (Mode 1): synthesize a from the agent's . +/// Prompt Agent (Mode 2, cached version): return the cached . +/// Prompt Agent (Mode 2, AgentReference-only): fetch the latest version from the service and return its definition. +/// Agent Endpoint (Mode 3): throw — no local definition exists to convert. +/// +/// +[Experimental(DiagnosticIds.Experiments.AIOpenAIResponses)] +internal static class FoundryPromptAgentConverter +{ + /// Performs the conversion for an agent whose chat client and chat options are supplied. + /// The chat client extracted from the calling agent (must surface a via ). + /// The agent's chat options (model id, instructions, temperature, top-p, tools). Required for the Responses Agent mode; ignored for the Prompt Agent mode. + /// A token that can cancel a server-side fetch (Prompt Agent AgentReference path). + /// A suitable for AgentAdministrationClient.CreateAgentVersionAsync. + /// Thrown when the chat client is not Foundry-backed, the agent was constructed via the Agent Endpoint mode, no model id is set for the Responses Agent mode, or an unsupported is encountered. + public static async Task ConvertAsync(IChatClient chatClient, ChatOptions? chatOptions, CancellationToken cancellationToken) + { + Throw.IfNull(chatClient); + + var foundryChatClient = chatClient.GetService() + ?? throw new InvalidOperationException( + "ToPromptAgentAsync requires a FoundryChatClient-backed agent. " + + "The supplied agent's chat client does not expose a FoundryChatClient via GetService()."); + + // Prompt Agent (Mode 2) with a cached server-side version (constructed via ProjectsAgentVersion or ProjectsAgentRecord). + if (foundryChatClient.GetService() is { } cachedVersion) + { + return cachedVersion.Definition; + } + + // Prompt Agent (Mode 2) AgentReference-only: fetch the agent definition from the service. + // Honor a pinned AgentReference.Version when present (Q-C fix); fall back to the latest + // version only when the reference is unpinned ("", null, or "latest"). + if (foundryChatClient.GetService() is { } agentReference) + { + var aiProjectClient = foundryChatClient.GetService() + ?? throw new InvalidOperationException( + "Cannot fetch the agent version because the FoundryChatClient does not expose an AIProjectClient."); + + if (!string.IsNullOrWhiteSpace(agentReference.Version) + && !string.Equals(agentReference.Version, "latest", StringComparison.OrdinalIgnoreCase)) + { + var pinnedVersion = await aiProjectClient.AgentAdministrationClient + .GetAgentVersionAsync(agentReference.Name, agentReference.Version, cancellationToken) + .ConfigureAwait(false); + return pinnedVersion.Value.Definition; + } + + var record = await aiProjectClient.AgentAdministrationClient + .GetAgentAsync(agentReference.Name, cancellationToken) + .ConfigureAwait(false); + return record.Value.GetLatestVersion().Definition; + } + + // Agent Endpoint (Mode 3): AgentName is set (parsed from URL) but no AgentReference exists + // locally. The agent definition lives only on the server and is not retrievable through this + // chat client, so conversion is not supported here. + if (foundryChatClient.AgentName is not null) + { + throw new InvalidOperationException( + "ToPromptAgentAsync is not supported for agents constructed via the Agent Endpoint mode (Mode 3); " + + "no local definition exists to convert."); + } + + // Responses Agent (Mode 1): synthesize from ChatOptions. + return SynthesizeFromChatOptions(chatOptions); + } + + private static DeclarativeAgentDefinition SynthesizeFromChatOptions(ChatOptions? chatOptions) + { + if (chatOptions is null || string.IsNullOrWhiteSpace(chatOptions.ModelId)) + { + throw new InvalidOperationException( + "ToPromptAgentAsync requires a model id on the agent's ChatOptions to synthesize a prompt agent definition."); + } + + var definition = new DeclarativeAgentDefinition(chatOptions.ModelId!) + { + Instructions = chatOptions.Instructions, + Temperature = chatOptions.Temperature, + TopP = chatOptions.TopP, + }; + + if (chatOptions.Tools is { Count: > 0 } tools) + { + foreach (var tool in tools) + { + definition.Tools.Add(ConvertTool(tool)); + } + } + + return definition; + } + + private static ResponseTool ConvertTool(AITool tool) + { + Throw.IfNull(tool); + + if (tool is AIFunction function) + { + // strictModeEnabled is intentionally true to match the Python spec's + // default behavior. JsonSchema on AIFunction is a JsonElement; serialize via its + // string form so the payload matches what callers pass elsewhere in this codebase. + return ResponseTool.CreateFunctionTool( + function.Name, + BinaryData.FromString(function.JsonSchema.ToString() ?? "{}"), + strictModeEnabled: true, + function.Description); + } + + if (tool.GetService(typeof(ResponseTool)) is ResponseTool responseTool) + { + return responseTool; + } + + throw new InvalidOperationException( + $"Cannot convert AITool of type '{tool.GetType().Name}' to a ResponseTool. " + + "Only AIFunction and AITool instances that wrap a ResponseTool (such as those produced by FoundryAITool factories) are supported."); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs deleted file mode 100644 index e00025b7ee..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Foundry/RequestOptionsExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI; - -internal static class RequestOptionsExtensions -{ - /// Gets the singleton that adds a MEAI user-agent header. - internal static PipelinePolicy UserAgentPolicy => MeaiUserAgentPolicy.Instance; - - /// Provides a pipeline policy that adds a "MEAI/x.y.z" user-agent header. - private sealed class MeaiUserAgentPolicy : PipelinePolicy - { - public static MeaiUserAgentPolicy Instance { get; } = new MeaiUserAgentPolicy(); - - private static readonly string s_userAgentValue = CreateUserAgentValue(); - - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - AddUserAgentHeader(message); - ProcessNext(message, pipeline, currentIndex); - } - - public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - AddUserAgentHeader(message); - return ProcessNextAsync(message, pipeline, currentIndex); - } - - private static void AddUserAgentHeader(PipelineMessage message) => - message.Request.Headers.Add("User-Agent", s_userAgentValue); - - private static string CreateUserAgentValue() - { - const string Name = "MEAI"; - - if (typeof(MeaiUserAgentPolicy).Assembly.GetCustomAttribute()?.InformationalVersion is string version) - { - int pos = version.IndexOf('+'); - if (pos >= 0) - { - version = version.Substring(0, pos); - } - - if (version.Length > 0) - { - return $"{Name}/{version}"; - } - } - - return Name; - } - } -} diff --git a/dotnet/tests/Foundry.IntegrationTests/FoundryAgentExtensionsTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryAgentExtensionsTests.cs new file mode 100644 index 0000000000..c5f5f3b9c5 --- /dev/null +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryAgentExtensionsTests.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Threading.Tasks; +using AgentConformance.IntegrationTests.Support; +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Foundry; +using OpenAI.Files; +using OpenAI.Responses; +using OpenAI.VectorStores; +using Shared.IntegrationTests; + +namespace Foundry.IntegrationTests; + +/// +/// Integration tests for the file and vector-store forwarder extensions on +/// declared in . End-to-end +/// counterparts of the unit tests in +/// FoundryAgentExtensionsTests that exercise the live Foundry project pipeline. +/// +/// +/// Mirrors +/// in shape (file upload → vector store creation → FileSearchTool answer → cleanup), but routes +/// every helper call through the new extensions instead of the raw +/// projectOpenAIClient.GetProjectFilesClient() / GetProjectVectorStoresClient() +/// path. Skipped by default for the same reasons as the existing vector-store IT (cost and +/// runtime); flip Skip to run manually after seeding the right Foundry project. +/// +public class FoundryAgentExtensionsTests +{ + private readonly AIProjectClient _client = new( + new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), + TestAzureCliCredentials.CreateAzureCliCredential()); + + [Fact(Skip = "For manual testing only")] + public async Task UploadFileAsync_ViaAgentExtension_UploadsToProjectAsync() + { + // Arrange — non-versioned Responses Agent (Mode 1) so we do not have to provision a server-side agent. + var agent = this._client.AsAIAgent( + model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), + instructions: "Be helpful."); + var foundryAgent = this.WrapAsFoundryAgent(agent); + + var filePath = Path.GetTempFileName() + ".txt"; + File.WriteAllText(filePath, "agent-extensions integration test payload"); + + OpenAIFile? uploaded = null; + try + { + // Act. + uploaded = await foundryAgent.UploadFileAsync(filePath, FileUploadPurpose.Assistants); + + // Assert. + Assert.NotNull(uploaded); + Assert.False(string.IsNullOrEmpty(uploaded.Id)); + Assert.Equal(Path.GetFileName(filePath), uploaded.Filename); + } + finally + { + if (uploaded is not null) + { + await foundryAgent.DeleteFileAsync(uploaded.Id); + } + + File.Delete(filePath); + } + } + + [Fact(Skip = "For manual testing only")] + public async Task DeleteFileAsync_ViaAgentExtension_RemovesUploadedFileAsync() + { + var agent = this._client.AsAIAgent( + model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), + instructions: "Be helpful."); + var foundryAgent = this.WrapAsFoundryAgent(agent); + + var filePath = Path.GetTempFileName() + ".txt"; + File.WriteAllText(filePath, "delete-me payload"); + + try + { + var uploaded = await foundryAgent.UploadFileAsync(filePath, FileUploadPurpose.Assistants); + + // Act. + var result = await foundryAgent.DeleteFileAsync(uploaded.Id); + + // Assert. + Assert.NotNull(result); + Assert.Equal(uploaded.Id, result.FileId); + Assert.True(result.Deleted); + } + finally + { + File.Delete(filePath); + } + } + + [Fact(Skip = "For manual testing only")] + public async Task CreateVectorStoreAsync_ViaAgentExtension_BuildsStoreAndAnswersFileSearchQuestionAsync() + { + // Mirrors CreateAgent_CreatesAgentWithVectorStoresAsync but the upload-then-create-store + // sequence routes through the FoundryAgent.CreateVectorStoreAsync extension (single call + // that uploads, creates the store, and polls until ready). The resulting vector store id + // is then wired to a versioned agent's FileSearch tool and queried for a known value. + string AgentName = FoundryVersionedAgentFixture.GenerateUniqueAgentName("VectorStoreExtAgent"); + const string AgentInstructions = """ + You are a helpful agent that can help fetch data from files you know about. + Use the File Search Tool to look up codes for words. + Do not answer a question unless you can find the answer using the File Search Tool. + """; + + // Non-versioned helper agent that owns the upload pipeline. + var helperAgent = this._client.AsAIAgent( + model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), + instructions: "Be helpful."); + var helperFoundryAgent = this.WrapAsFoundryAgent(helperAgent); + + var searchFilePath = Path.GetTempFileName() + "wordcodelookup.txt"; + File.WriteAllText(searchFilePath, "The word 'apple' uses the code 442345, while the word 'banana' uses the code 673457."); + + VectorStore? vectorStore = null; + FoundryAgent? versionedAgent = null; + try + { + // Act — single agent-level helper call uploads, creates, and waits until ready. + vectorStore = await helperFoundryAgent.CreateVectorStoreAsync( + "WordCodeLookup_ExtensionVectorStore", + new[] { searchFilePath }); + + Assert.NotNull(vectorStore); + Assert.False(string.IsNullOrEmpty(vectorStore.Id)); + Assert.NotEqual(VectorStoreStatus.InProgress, vectorStore.Status); + + // Wire the store id into a versioned agent's FileSearch tool to prove it is actually usable. + var definition = new DeclarativeAgentDefinition(TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName)) + { + Instructions = AgentInstructions, + Tools = { ResponseTool.CreateFileSearchTool(vectorStoreIds: [vectorStore.Id]) }, + }; + + var agentVersion = await this._client.AgentAdministrationClient.CreateAgentVersionAsync( + AgentName, + new ProjectsAgentVersionCreationOptions(definition)); + + versionedAgent = this._client.AsAIAgent(agentVersion); + + // Assert. + var result = await versionedAgent.RunAsync("Can you give me the documented code for 'banana'?"); + Assert.Contains("673457", result.ToString()); + } + finally + { + if (versionedAgent is not null) + { + await this._client.AgentAdministrationClient.DeleteAgentAsync(versionedAgent.Name); + } + + // Cleanup the vector store via the new extension too. + if (vectorStore is not null) + { + await helperFoundryAgent.DeleteVectorStoreAsync(vectorStore.Id); + } + + File.Delete(searchFilePath); + } + } + + [Fact(Skip = "For manual testing only")] + public async Task DeleteVectorStoreAsync_ViaAgentExtension_RemovesStoreAsync() + { + var agent = this._client.AsAIAgent( + model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), + instructions: "Be helpful."); + var foundryAgent = this.WrapAsFoundryAgent(agent); + + var filePath = Path.GetTempFileName() + ".txt"; + File.WriteAllText(filePath, "delete-store payload"); + + VectorStore? vectorStore = null; + try + { + vectorStore = await foundryAgent.CreateVectorStoreAsync( + "DeleteVectorStore_ExtensionTest", + new[] { filePath }); + + // Act. + var result = await foundryAgent.DeleteVectorStoreAsync(vectorStore.Id); + + // Assert. + Assert.NotNull(result); + Assert.Equal(vectorStore.Id, result.VectorStoreId); + Assert.True(result.Deleted); + vectorStore = null; + } + finally + { + if (vectorStore is not null) + { + await foundryAgent.DeleteVectorStoreAsync(vectorStore.Id); + } + + File.Delete(filePath); + } + } + + /// + /// Resolves the underlying from an handle + /// returned by AIProjectClient.AsAIAgent(model, instructions). The Mode 1 overload + /// returns a ; the extension forwarders we test live on + /// , so callers wanting them through this entry point need to + /// reach for the FoundryAgent constructor instead. This helper makes the test setup + /// consistent across the four IT scenarios. + /// + private FoundryAgent WrapAsFoundryAgent(AIAgent agent) + { + // The Mode 1 AsAIAgent overload returns ChatClientAgent rather than FoundryAgent; use + // the FoundryAgent projectEndpoint+model+instructions ctor to get the same underlying + // FoundryChatClient surfaced through a FoundryAgent typed handle. + _ = agent; + return new FoundryAgent( + projectEndpoint: new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), + credential: TestAzureCliCredentials.CreateAzureCliCredential(), + model: TestConfiguration.GetRequiredValue(TestSettings.AzureAIModelDeploymentName), + instructions: "Be helpful."); + } +} diff --git a/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs index 015877df05..200df19b16 100644 --- a/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs +++ b/dotnet/tests/Foundry.IntegrationTests/FoundryVersionedAgentStructuredOutputRunTests.cs @@ -11,7 +11,7 @@ namespace Foundry.IntegrationTests; public class FoundryVersionedAgentStructuredOutputRunTests() : StructuredOutputRunTests>(() => new FoundryVersionedAgentStructuredOutputFixture()) { private const string NotSupported = "Versioned Foundry agents do not support specifying structured output type at invocation time."; - private const string ResponseFormatNotSupported = "AzureAIProjectChatClient clears ResponseFormat for versioned agents; structured output must be defined in the server-side agent definition."; + private const string ResponseFormatNotSupported = "FoundryChatClient clears ResponseFormat for versioned agents; structured output must be defined in the server-side agent definition."; /// /// Verifies that response format provided at agent initialization is used when invoking RunAsync. @@ -41,7 +41,7 @@ public class FoundryVersionedAgentStructuredOutputRunTests() : StructuredOutputR /// /// /// Versioned Foundry agents do not support specifying the structured output type at invocation time yet. - /// The type T provided to RunAsync<T> is ignored by AzureAIProjectChatClient and is only used + /// The type T provided to RunAsync<T> is ignored by FoundryChatClient and is only used /// for deserializing the agent response by AgentResponse<T>.Result. /// [RetryFact(Constants.RetryCount, Constants.RetryDelay, Skip = ResponseFormatNotSupported)] diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs index 844911055f..090633f7d5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedOutboundUserAgentTests.cs @@ -64,15 +64,29 @@ public sealed class HostedOutboundUserAgentTests : IAsyncDisposable var inboundBody = await inboundResponse.Content.ReadAsStringAsync(); // Assert: at least one OUTBOUND request reached the fake transport, AND it carries the - // foundry-hosting/agent-framework-dotnet/{version} supplement on its User-Agent. - // (We don't care about the inbound response shape — only that the agent's call to MEAI - // triggered an outbound request whose UA reaches the sandbox boundary correctly.) + // combined hosted segment foundry-hosting/agent-framework-dotnet/{version} on its + // User-Agent. This matches Python's contract + // (foundry-hosting/agent-framework-python/{version}, see + // python/packages/core/agent_framework/_telemetry.py): a single combined segment when + // hosted, never two separate ones. The bare agent-framework-dotnet/{version} segment + // (from AgentFrameworkUserAgentPolicy in FoundryChatClient) must be upgraded in place + // by HostedAgentUserAgentPolicy — never appear duplicated. Assert.True(this._outboundHandler!.Requests.Count > 0, $"Expected at least one outbound request. Inbound status: {(int)inboundResponse.StatusCode}, body: {inboundBody}"); var outbound = this._outboundHandler.Requests[0]; Assert.StartsWith(TestEndpoint, outbound.Uri); Assert.Contains("MEAI/", outbound.UserAgent); - Assert.Contains("foundry-hosting/agent-framework-dotnet", outbound.UserAgent); + Assert.Contains("foundry-hosting/agent-framework-dotnet/", outbound.UserAgent); + + // The bare agent-framework-dotnet/{v} segment must NOT appear separately when the + // combined form is present — Python emits a single combined value when the hosted + // prefix is registered, and .NET preserves that contract via the in-place upgrade in + // HostedAgentUserAgentPolicy. + var combinedIdx = outbound.UserAgent!.IndexOf("foundry-hosting/agent-framework-dotnet/", StringComparison.Ordinal); + var beforeCombined = outbound.UserAgent.Substring(0, combinedIdx); + var afterCombined = outbound.UserAgent.Substring(combinedIdx + "foundry-hosting/agent-framework-dotnet/".Length); + Assert.DoesNotContain("agent-framework-dotnet/", beforeCombined); + Assert.DoesNotContain("agent-framework-dotnet/", afterCombined); } private async Task StartHostedServerAsync() @@ -197,6 +211,179 @@ public sealed class HostedOutboundUserAgentTests : IAsyncDisposable return array?.Length ?? -1; } + // ----------------------------------------------------------------------- + // Direct unit tests for HostedAgentUserAgentPolicy's in-place upgrade behavior. + // These run the policy on a synthetic ClientPipeline (no hosting infrastructure) + // so the upgrade logic itself can be asserted in isolation. + // ----------------------------------------------------------------------- + + [Fact] + public async Task HostedAgentUserAgentPolicy_UpgradesBareAgentFrameworkSegment_InPlaceAsync() + { + // Arrange: an upstream per-call policy stamps the bare agent-framework-dotnet/{version} + // segment (matching what AgentFrameworkUserAgentPolicy would write in non-hosted code). + // Then HostedAgentUserAgentPolicy runs and must REPLACE that segment with the combined + // foundry-hosting/agent-framework-dotnet/{version} form, not append a duplicate. + using var handler = new InspectingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [new SetUserAgentPolicy("agent-framework-dotnet/9.9.9"), HostedAgentUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert: combined form is present; bare form is gone (no duplicate agent-framework segment). + Assert.NotNull(handler.LastUserAgent); + Assert.Contains("foundry-hosting/agent-framework-dotnet/", handler.LastUserAgent); + var ua = handler.LastUserAgent!; + var firstAgentFramework = ua.IndexOf("agent-framework-dotnet/", StringComparison.Ordinal); + Assert.True(firstAgentFramework >= 0, "Expected agent-framework-dotnet segment."); + var secondAgentFramework = ua.IndexOf("agent-framework-dotnet/", firstAgentFramework + 1, StringComparison.Ordinal); + Assert.Equal(-1, secondAgentFramework); + } + + [Fact] + public async Task HostedAgentUserAgentPolicy_AppendsCombined_WhenNoBareSegmentPresentAsync() + { + // Arrange: nothing upstream stamps the bare segment. Hosted policy should append the + // full combined segment to whatever User-Agent is on the wire. + using var handler = new InspectingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [HostedAgentUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert + Assert.NotNull(handler.LastUserAgent); + Assert.Contains("foundry-hosting/agent-framework-dotnet/", handler.LastUserAgent); + } + + [Fact] + public async Task HostedAgentUserAgentPolicy_IsIdempotent_WhenCombinedSegmentAlreadyPresentAsync() + { + // Arrange: upstream pre-populates the combined segment (simulating a retry or duplicate + // registration). Hosted policy must not re-append. + using var handler = new InspectingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [new SetUserAgentPolicy("foundry-hosting/agent-framework-dotnet/9.9.9"), HostedAgentUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert: exactly one occurrence of "foundry-hosting/agent-framework-dotnet/" segment. + Assert.NotNull(handler.LastUserAgent); + var first = handler.LastUserAgent!.IndexOf("foundry-hosting/agent-framework-dotnet/", StringComparison.Ordinal); + Assert.True(first >= 0); + var second = handler.LastUserAgent.IndexOf("foundry-hosting/agent-framework-dotnet/", first + 1, StringComparison.Ordinal); + Assert.Equal(-1, second); + } + + [Fact] + public async Task HostedAgentUserAgentPolicy_ReplacesDifferentVersionCombinedSegment_InPlaceAsync() + { + // Q-D regression: when the User-Agent already carries the COMBINED hosted form with a + // different version (e.g. an older registration or caller-supplied baseline), the policy + // must replace the entire combined span — not just the bare suffix — so we never emit + // the malformed `foundry-hosting/foundry-hosting/agent-framework-dotnet/...` shape. + using var handler = new InspectingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [new SetUserAgentPolicy("foundry-hosting/agent-framework-dotnet/0.0.1 MEAI/10.5.1"), HostedAgentUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert: no doubled foundry-hosting/ prefix. + Assert.NotNull(handler.LastUserAgent); + Assert.DoesNotContain("foundry-hosting/foundry-hosting/", handler.LastUserAgent, StringComparison.Ordinal); + + // The combined segment must appear exactly once, and the trailing MEAI segment must be + // preserved in place (i.e. the policy only rewrote the combined span, not anything after it). + var firstCombined = handler.LastUserAgent!.IndexOf("foundry-hosting/agent-framework-dotnet/", StringComparison.Ordinal); + Assert.True(firstCombined >= 0); + var secondCombined = handler.LastUserAgent.IndexOf("foundry-hosting/agent-framework-dotnet/", firstCombined + 1, StringComparison.Ordinal); + Assert.Equal(-1, secondCombined); + Assert.Contains(" MEAI/10.5.1", handler.LastUserAgent, StringComparison.Ordinal); + + // And the version that survives must be the runtime supplement value's version, not 0.0.1. + Assert.DoesNotContain("foundry-hosting/agent-framework-dotnet/0.0.1", handler.LastUserAgent, StringComparison.Ordinal); + } + + private sealed class InspectingHandler : HttpClientHandler + { + public string? LastUserAgent { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.LastUserAgent = request.Headers.TryGetValues("User-Agent", out var values) + ? string.Join(",", values) + : null; + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}", Encoding.UTF8, "application/json"), + RequestMessage = request, + }); + } + } + + private sealed class SetUserAgentPolicy : PipelinePolicy + { + private readonly string _value; + public SetUserAgentPolicy(string value) => this._value = value; + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + message.Request.Headers.Set("User-Agent", this._value); + ProcessNext(message, pipeline, currentIndex); + } + + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + message.Request.Headers.Set("User-Agent", this._value); + return ProcessNextAsync(message, pipeline, currentIndex); + } + } + private sealed class NoopHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AIProjectClientExtensionsTests.cs similarity index 93% rename from dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AIProjectClientExtensionsTests.cs index 2996be725d..f4da9e6b77 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AIProjectClientExtensionsTests.cs @@ -23,9 +23,9 @@ namespace Microsoft.Agents.AI.Foundry.UnitTests; #pragma warning disable CS0618 /// -/// Unit tests for the class. +/// Unit tests for the class. /// -public sealed class AzureAIProjectChatClientExtensionsTests +public sealed class AIProjectClientExtensionsTests { #region AsAIAgent(AIProjectClient, model, instructions) Tests @@ -71,7 +71,11 @@ public sealed class AzureAIProjectChatClientExtensionsTests Assert.Equal("test-agent", agent.Name); Assert.Equal("A test agent", agent.Description); Assert.NotNull(agent.GetService()); - Assert.Null(agent.GetService()); + // After the FoundryChatClient consolidation the inner chat-client now exposes the + // AIProjectClient via GetService — Foundry callers can walk to the project client from + // the agent without holding their own reference. (Previously this path returned null + // because AsAIAgent(model, instructions) skipped the decorator entirely.) + Assert.NotNull(agent.GetService()); } /// @@ -123,7 +127,10 @@ public sealed class AzureAIProjectChatClientExtensionsTests Assert.NotNull(agent); Assert.Equal("options-agent", agent.Name); Assert.Equal("Agent from options", agent.Description); - Assert.Null(agent.GetService()); + // After the FoundryChatClient consolidation the inner chat-client now exposes the + // AIProjectClient via GetService — see twin assertion in + // AsAIAgent_Rapi_WithModelAndInstructions_CreatesChatClientAgent for the rationale. + Assert.NotNull(agent.GetService()); } /// @@ -185,6 +192,106 @@ public sealed class AzureAIProjectChatClientExtensionsTests Assert.True(userAgentFound, "MEAI user-agent header was not found in any request"); } + /// + /// Verify that the non-versioned AsAIAgent overload now wraps with FoundryChatClient + /// (regression-prevention for the previously-untagged extension path). + /// + [Fact] + public void AsAIAgent_Rapi_WithModelAndInstructions_ExposesFoundryChatClientAndProviderName() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + + // Act + ChatClientAgent agent = client.AsAIAgent("gpt-4o-mini", "You are helpful."); + + // Assert: FoundryChatClient is internal-sealed and reachable via GetService(). + var chatClient = agent.GetService(); + Assert.NotNull(chatClient); + + // Provider tag is "microsoft.foundry" (previously this path had no Foundry tag at all). + var metadata = chatClient!.GetService(); + Assert.NotNull(metadata); + Assert.Equal("microsoft.foundry", metadata!.ProviderName); + Assert.Equal("gpt-4o-mini", metadata.DefaultModelId); + + // Reaching the FoundryChatClient by type (via InternalsVisibleTo). + Assert.NotNull(agent.GetService()); + } + + /// + /// Verify that the options-based non-versioned AsAIAgent overload now wraps with FoundryChatClient. + /// + [Fact] + public void AsAIAgent_Rapi_WithOptions_ExposesFoundryChatClientAndProviderName() + { + // Arrange + AIProjectClient client = this.CreateTestAgentClient(); + ChatClientAgentOptions options = new() + { + Name = "options-agent", + ChatOptions = new ChatOptions { ModelId = "gpt-4o-mini", Instructions = "x" }, + }; + + // Act + ChatClientAgent agent = client.AsAIAgent(options); + + // Assert + var chatClient = agent.GetService(); + Assert.NotNull(chatClient); + var metadata = chatClient!.GetService(); + Assert.NotNull(metadata); + Assert.Equal("microsoft.foundry", metadata!.ProviderName); + Assert.NotNull(agent.GetService()); + } + + /// + /// Verify that the non-versioned AsAIAgent overload stamps the + /// agent-framework-dotnet/{version} segment on outbound requests via the new + /// AgentFrameworkUserAgentPolicy registered by FoundryChatClient. + /// + [Fact] + public async Task AsAIAgent_Rapi_WithModelAndInstructions_StampsAgentFrameworkUserAgentSegmentAsync() + { + bool afSeen = false; + using HttpHandlerAssert httpHandler = new(request => + { + if (request.Headers.TryGetValues("User-Agent", out IEnumerable? values)) + { + foreach (string value in values) + { + if (value.Contains("agent-framework-dotnet/")) + { + afSeen = true; + } + } + } + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") + }; + }); + +#pragma warning disable CA5399 + using HttpClient httpClient = new(httpHandler); +#pragma warning restore CA5399 + + AIProjectClient aiProjectClient = new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new() { Transport = new HttpClientPipelineTransport(httpClient) }); + + ChatClientAgent agent = aiProjectClient.AsAIAgent("gpt-4o-mini", "You are helpful."); + + // Act + AgentSession session = await agent.CreateSessionAsync(); + await agent.RunAsync("Hello", session); + + // Assert + Assert.True(afSeen, "Expected agent-framework-dotnet/{version} segment on outbound requests from AsAIAgent(model, instructions)."); + } + #endregion #region AsAIAgent(AIProjectClient, ProjectsAgentRecord) Tests diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AgentFrameworkUserAgentPolicyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AgentFrameworkUserAgentPolicyTests.cs new file mode 100644 index 0000000000..94999cfb64 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AgentFrameworkUserAgentPolicyTests.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Agents.AI.Foundry.UnitTests; + +/// +/// Verifies the framework-wide . The policy stamps +/// agent-framework-dotnet/{version} onto the outgoing User-Agent header of every +/// request made through a Foundry chat client and is registered automatically by +/// FoundryChatClient via the MEAI OpenAIRequestPolicies hook. +/// +public sealed class AgentFrameworkUserAgentPolicyTests +{ + [Fact] + public async Task AgentFrameworkUserAgentPolicy_AddsAgentFrameworkSegment_ToOutgoingRequestAsync() + { + // Arrange + using var handler = new RecordingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [AgentFrameworkUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert + Assert.Equal(1, handler.Count); + Assert.NotNull(handler.LastUserAgent); + Assert.Contains("agent-framework-dotnet/", handler.LastUserAgent); + } + + [Fact] + public async Task AgentFrameworkUserAgentPolicy_DoesNotStampMeaiSegmentAsync() + { + // Arrange: the AF policy must only contribute the agent-framework-dotnet segment. + // The MEAI/{version} segment is contributed by the MEAI-shipped policy at a different + // layer; this policy must not duplicate or replace it. + using var handler = new RecordingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [AgentFrameworkUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert + Assert.NotNull(handler.LastUserAgent); + Assert.DoesNotContain("MEAI/", handler.LastUserAgent); + Assert.DoesNotContain("foundry-hosting/", handler.LastUserAgent); + } + + [Fact] + public async Task AgentFrameworkUserAgentPolicy_PreservesExistingUserAgent_WhenAppendingAsync() + { + // Arrange: a per-call policy upstream that pre-populates the User-Agent header. The AF + // policy must read the existing value and append (not overwrite) the agent-framework + // segment so both stay reachable on the wire. (The exact separator the HTTP transport + // emits between multi-value User-Agent entries is comma per RFC 7230; this test does + // not assert on the separator character because that is a transport detail.) + using var handler = new RecordingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [new SeedUserAgentPolicy("existing-app/1.0"), AgentFrameworkUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert: both segments survive to the wire. + Assert.NotNull(handler.LastUserAgent); + Assert.Contains("existing-app/1.0", handler.LastUserAgent); + Assert.Contains("agent-framework-dotnet/", handler.LastUserAgent); + } + + [Fact] + public async Task AgentFrameworkUserAgentPolicy_IsIdempotent_DoesNotDoubleStampAsync() + { + // Arrange: register the same policy twice on the same pipeline. The second application + // must detect the segment is already present and not append it again. Guards against + // double-stamping on retries or duplicate registration. + using var handler = new RecordingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var pipeline = ClientPipeline.Create( + new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, + perCallPolicies: [AgentFrameworkUserAgentPolicy.Instance, AgentFrameworkUserAgentPolicy.Instance], + perTryPolicies: default, + beforeTransportPolicies: default); + + // Act + var message = pipeline.CreateMessage(); + message.Request.Method = "POST"; + message.Request.Uri = new Uri("https://example.test/anything"); + await pipeline.SendAsync(message); + + // Assert: exactly one occurrence of "agent-framework-dotnet/". + Assert.NotNull(handler.LastUserAgent); + var ua = handler.LastUserAgent!; + var first = ua.IndexOf("agent-framework-dotnet/", StringComparison.Ordinal); + Assert.True(first >= 0, "Expected at least one agent-framework-dotnet segment."); + var second = ua.IndexOf("agent-framework-dotnet/", first + 1, StringComparison.Ordinal); + Assert.Equal(-1, second); + } + + [Fact] + public void AgentFrameworkUserAgentPolicy_ExposesSingletonInstance() + { + // Two reads of the static property must return the same instance. The policy is stateless + // and shared; allocating a fresh instance per registration site would bloat memory and + // defeat the dedup logic in OpenAIRequestPoliciesReflection.AddPolicyIfMissing. + var first = AgentFrameworkUserAgentPolicy.Instance; + var second = AgentFrameworkUserAgentPolicy.Instance; + Assert.Same(first, second); + } + + [Fact] + public void AgentFrameworkUserAgentPolicy_ValueIncludesAFFoundryAssemblyVersion_ReflectionGuard() + { + // The policy emits "agent-framework-dotnet/{Microsoft.Agents.AI.Foundry assembly InformationalVersion}". + // If the assembly metadata stops being readable, the policy falls back to "agent-framework-dotnet" + // without a version, which is a measurable telemetry regression. + var attr = typeof(AgentFrameworkUserAgentPolicy).Assembly + .GetCustomAttribute(); + Assert.NotNull(attr); + Assert.False(string.IsNullOrEmpty(attr!.InformationalVersion)); + } + + private sealed class RecordingHandler : HttpClientHandler + { + public int Count { get; private set; } + public string? LastUserAgent { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.Count++; + this.LastUserAgent = request.Headers.TryGetValues("User-Agent", out var values) + ? string.Join(",", values) + : null; + + var resp = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}", Encoding.UTF8, "application/json"), + RequestMessage = request, + }; + return Task.FromResult(resp); + } + } + + private sealed class SeedUserAgentPolicy : PipelinePolicy + { + private readonly string _value; + public SeedUserAgentPolicy(string value) => this._value = value; + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + message.Request.Headers.Set("User-Agent", this._value); + ProcessNext(message, pipeline, currentIndex); + } + + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + message.Request.Headers.Set("User-Agent", this._value); + return ProcessNextAsync(message, pipeline, currentIndex); + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs deleted file mode 100644 index e3461d8191..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/AzureAIProjectChatClientTests.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ClientModel.Primitives; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Azure.AI.Extensions.OpenAI; -using Azure.AI.Projects; - -namespace Microsoft.Agents.AI.Foundry.UnitTests; - -#pragma warning disable CS0618 -public class AzureAIProjectChatClientTests -{ - /// - /// Verify that after the first RunAsync, the session's ConversationId is set from the - /// response, and subsequent requests include that conversation ID automatically. - /// - [Fact] - public async Task ChatClient_UsesDefaultConversationIdAsync() - { - // Arrange - var responsesRequestCount = 0; - using var httpHandler = new HttpHandlerAssert(async (request) => - { - if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) - { - responsesRequestCount++; - - // Assert: On the second Responses API call, verify the conversation ID - // from the first response is automatically included in the request body. - if (responsesRequestCount == 2 && request.Content is not null) - { - var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); - Assert.Contains("resp_0888a", requestBody); - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; - }); - -#pragma warning disable CA5399 - using var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - AIProjectClient projectClient = new( - new Uri("https://test.openai.azure.com/"), - new FakeAuthenticationTokenProvider(), - new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - - var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); - - // Act - var session = await agent.CreateSessionAsync(); - await agent.RunAsync("Hello", session); - await agent.RunAsync("Follow up", session); - - // Assert - Assert.Equal(2, responsesRequestCount); - var chatClientSession = Assert.IsType(session); - Assert.Equal("resp_0888a46cbf2b1ff3006914596e05d08195a77c3f5187b769a7", chatClientSession.ConversationId); - } - - /// - /// Verify that when the chat client doesn't have a default "conv_" conversation id, the chat client still uses the conversation ID in HTTP requests. - /// - [Fact] - public async Task ChatClient_UsesPerRequestConversationId_WhenNoDefaultConversationIdIsProvidedAsync() - { - // Arrange - var requestTriggered = false; - using var httpHandler = new HttpHandlerAssert(async (request) => - { - if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) - { - requestTriggered = true; - - // Assert - if (request.Content is not null) - { - var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); - Assert.Contains("conv_12345", requestBody); - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; - }); - -#pragma warning disable CA5399 - using var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - AIProjectClient projectClient = new( - new Uri("https://test.openai.azure.com/"), - new FakeAuthenticationTokenProvider(), - new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - - var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); - - // Act - var session = await agent.CreateSessionAsync(); - await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } }); - - Assert.True(requestTriggered); - var chatClientSession = Assert.IsType(session); - Assert.Equal("conv_12345", chatClientSession.ConversationId); - } - - /// - /// Verify that even when the chat client has a default conversation id, the chat client will prioritize the per-request conversation id provided in HTTP requests. - /// - [Fact] - public async Task ChatClient_UsesPerRequestConversationId_EvenWhenDefaultConversationIdIsProvidedAsync() - { - // Arrange - var requestTriggered = false; - using var httpHandler = new HttpHandlerAssert(async (request) => - { - if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) - { - requestTriggered = true; - - // Assert - if (request.Content is not null) - { - var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); - Assert.Contains("conv_12345", requestBody); - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; - }); - -#pragma warning disable CA5399 - using var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - AIProjectClient projectClient = new( - new Uri("https://test.openai.azure.com/"), - new FakeAuthenticationTokenProvider(), - new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - - var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); - - // Act - var session = await agent.CreateSessionAsync(); - await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } }); - - Assert.True(requestTriggered); - var chatClientSession = Assert.IsType(session); - Assert.Equal("conv_12345", chatClientSession.ConversationId); - } - - /// - /// Verify that when the chat client is provided without a "conv_" prefixed conversation ID, the chat client uses the previous conversation ID in HTTP requests. - /// - [Fact] - public async Task ChatClient_UsesPreviousResponseId_WhenConversationIsNotPrefixedAsConvAsync() - { - // Arrange - var requestTriggered = false; - using var httpHandler = new HttpHandlerAssert(async (request) => - { - if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) - { - requestTriggered = true; - - // Assert - if (request.Content is not null) - { - var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); - Assert.Contains("resp_0888a", requestBody); - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; - } - - return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; - }); - -#pragma warning disable CA5399 - using var httpClient = new HttpClient(httpHandler); -#pragma warning restore CA5399 - - AIProjectClient projectClient = new( - new Uri("https://test.openai.azure.com/"), - new FakeAuthenticationTokenProvider(), - new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); - - var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); - - // Act - var session = await agent.CreateSessionAsync(); - await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "resp_0888a" } }); - - Assert.True(requestTriggered); - var chatClientSession = Assert.IsType(session); - Assert.Equal("resp_0888a46cbf2b1ff3006914596e05d08195a77c3f5187b769a7", chatClientSession.ConversationId); - } -} -#pragma warning restore CS0618 diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentExtensionsTests.cs new file mode 100644 index 0000000000..27979c1c6d --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentExtensionsTests.cs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel.Primitives; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Azure.AI.Projects; +using OpenAI.Files; + +#pragma warning disable OPENAI001, CS0618 + +namespace Microsoft.Agents.AI.Foundry.UnitTests; + +/// +/// Unit tests for the file and vector-store forwarder extensions on +/// declared in . The forwarders are thin shims over the +/// inner , so coverage focuses on (a) request shape (the agent +/// path reaches the same wire as a direct chat-client call), (b) null/missing-FoundryChatClient +/// handling, and (c) returns the same payload the chat client would. +/// +public sealed class FoundryAgentExtensionsTests +{ + private static readonly Uri s_testProjectEndpoint = new("https://test.openai.azure.com/"); + + [Fact] + public async Task UploadFileAsync_Forwards_ToInnerFoundryChatClient_Async() + { + // Arrange — agent built via the Responses Agent (Mode 1) projectEndpoint+model+instructions + // ctor wires a FoundryChatClient inside that the extension can resolve via GetService. + var sawPostToFiles = false; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Post && req.RequestUri!.AbsolutePath.Contains("/files", StringComparison.Ordinal)) + { + sawPostToFiles = true; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(FakeFileJson("file_via_agent"), Encoding.UTF8, "application/json"), + }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var agent = new FoundryAgent( + projectEndpoint: s_testProjectEndpoint, + credential: new FakeAuthenticationTokenProvider(), + model: "gpt-4o-mini", + instructions: "Be helpful.", + clientOptions: new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + + var path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"fae-{Guid.NewGuid():N}.txt"); + System.IO.File.WriteAllText(path, "hello"); + + try + { + // Act — call the forwarder on the agent. + var result = await agent.UploadFileAsync(path, FileUploadPurpose.Assistants); + + // Assert + Assert.True(sawPostToFiles, "POST to /files must reach the wire through the agent forwarder."); + Assert.Equal("file_via_agent", result.Id); + } + finally + { + System.IO.File.Delete(path); + } + } + + [Fact] + public async Task DeleteFileAsync_Forwards_ToInnerFoundryChatClient_Async() + { + var sawDelete = false; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Delete && req.RequestUri!.AbsolutePath.Contains("/files/", StringComparison.Ordinal)) + { + sawDelete = true; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"id\":\"file_abc\",\"object\":\"file\",\"deleted\":true}", Encoding.UTF8, "application/json"), + }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var agent = new FoundryAgent( + projectEndpoint: s_testProjectEndpoint, + credential: new FakeAuthenticationTokenProvider(), + model: "gpt-4o-mini", + instructions: "Be helpful.", + clientOptions: new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + + var result = await agent.DeleteFileAsync("file_abc"); + + Assert.True(sawDelete); + Assert.NotNull(result); + } + + [Fact] + public async Task CreateVectorStoreAsync_Forwards_ToInnerFoundryChatClient_Async() + { + var sawVectorStorePost = false; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Post && req.RequestUri!.AbsolutePath.Contains("/vector_stores", StringComparison.Ordinal)) + { + sawVectorStorePost = true; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(FakeVectorStoreJson("vs_via_agent", "kb"), Encoding.UTF8, "application/json"), + }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var agent = new FoundryAgent( + projectEndpoint: s_testProjectEndpoint, + credential: new FakeAuthenticationTokenProvider(), + model: "gpt-4o-mini", + instructions: "Be helpful.", + clientOptions: new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + + var store = await agent.CreateVectorStoreAsync("kb", Array.Empty()); + + Assert.True(sawVectorStorePost); + Assert.Equal("vs_via_agent", store.Id); + } + + [Fact] + public async Task DeleteVectorStoreAsync_Forwards_ToInnerFoundryChatClient_Async() + { + var sawDelete = false; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Delete && req.RequestUri!.AbsolutePath.Contains("/vector_stores/", StringComparison.Ordinal)) + { + sawDelete = true; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"id\":\"vs_abc\",\"object\":\"vector_store.deleted\",\"deleted\":true}", Encoding.UTF8, "application/json"), + }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var agent = new FoundryAgent( + projectEndpoint: s_testProjectEndpoint, + credential: new FakeAuthenticationTokenProvider(), + model: "gpt-4o-mini", + instructions: "Be helpful.", + clientOptions: new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + + await agent.DeleteVectorStoreAsync("vs_abc"); + + Assert.True(sawDelete); + } + + [Fact] + public async Task UploadFileAsync_NullAgent_ThrowsArgumentNullExceptionAsync() + => await Assert.ThrowsAsync(() => + FoundryAgentExtensions.UploadFileAsync(null!, "x", FileUploadPurpose.Assistants)); + + [Fact] + public async Task DeleteFileAsync_NullAgent_ThrowsArgumentNullExceptionAsync() + => await Assert.ThrowsAsync(() => + FoundryAgentExtensions.DeleteFileAsync(null!, "file_abc")); + + [Fact] + public async Task CreateVectorStoreAsync_NullAgent_ThrowsArgumentNullExceptionAsync() + => await Assert.ThrowsAsync(() => + FoundryAgentExtensions.CreateVectorStoreAsync(null!, "kb", Array.Empty())); + + [Fact] + public async Task DeleteVectorStoreAsync_NullAgent_ThrowsArgumentNullExceptionAsync() + => await Assert.ThrowsAsync(() => + FoundryAgentExtensions.DeleteVectorStoreAsync(null!, "vs_abc")); + + // ----- Helpers ----- + + private static string FakeFileJson(string id) + => $"{{\"id\":\"{id}\",\"object\":\"file\",\"bytes\":11,\"created_at\":1700000000,\"filename\":\"x.txt\",\"purpose\":\"assistants\",\"status\":\"processed\"}}"; + + private static string FakeVectorStoreJson(string id, string name) + => $"{{\"id\":\"{id}\",\"object\":\"vector_store\",\"created_at\":1700000000,\"name\":\"{name}\",\"usage_bytes\":0,\"file_counts\":{{\"in_progress\":0,\"completed\":0,\"failed\":0,\"cancelled\":0,\"total\":0}},\"status\":\"completed\",\"last_active_at\":1700000000}}"; +} +#pragma warning restore CS0618 diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs index 11f36c2797..1d88809e9f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryAgentTests.cs @@ -2,7 +2,6 @@ using System; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Text; @@ -352,18 +351,24 @@ public class FoundryAgentTests } [Fact] - public async Task Constructor_UserAgentHeaderAddedToRequestsAsync() + public async Task Constructor_AgentFrameworkUserAgentHeaderAddedToRequestsAsync() { - bool userAgentFound = false; + // After the FoundryChatClient consolidation, every outbound request from a + // FoundryAgent-built chat client carries the new agent-framework-dotnet/{version} + // segment (stamped by AgentFrameworkUserAgentPolicy registered via the MEAI + // OpenAIRequestPolicies hook). The local MEAI/{version} stamp was removed because + // MEAI 10.5.1 stamps that itself; this test only verifies the framework-wide segment + // that the Foundry package now guarantees. + bool agentFrameworkUserAgentFound = false; using HttpHandlerAssert httpHandler = new(request => { - if (request.Headers.TryGetValues("User-Agent", out IEnumerable? values)) + if (request.Headers.TryGetValues("User-Agent", out System.Collections.Generic.IEnumerable? values)) { foreach (string value in values) { - if (value.StartsWith("MEAI/", StringComparison.OrdinalIgnoreCase)) + if (value.Contains("agent-framework-dotnet/")) { - userAgentFound = true; + agentFrameworkUserAgentFound = true; } } } @@ -396,7 +401,7 @@ public class FoundryAgentTests AgentSession session = await agent.CreateSessionAsync(); await agent.RunAsync("Hello", session); - Assert.True(userAgentFound, "Expected MEAI user-agent header to be present in requests."); + Assert.True(agentFrameworkUserAgentFound, "Expected agent-framework-dotnet user-agent segment to be present on outbound requests."); } #endregion @@ -434,6 +439,9 @@ public class FoundryAgentTests [Fact] public void AgentEndpointConstructor_GetServiceProjectOpenAIClient_ReturnsNull() { + // Behavior change: FoundryAgent no longer caches a ProjectOpenAIClient. Callers + // retrieve it from the AIProjectClient themselves + // (agent.GetService()!.GetProjectOpenAIClient()). FoundryAgent agent = new(s_testAgentEndpoint, new FakeAuthenticationTokenProvider()); Assert.Null(agent.GetService()); @@ -442,6 +450,10 @@ public class FoundryAgentTests [Fact] public void AgentEndpointConstructor_GetServiceAIProjectClient_ReturnsNonNull() { + // Behavior change: after Plan #2's Agent Endpoint mode (Mode 3) AIProjectClient materialization, the + // agent-endpoint constructor now derives a project-level AIProjectClient from the + // parsed project root URL and surfaces it via GetService. Previously this returned + // null because no AIProjectClient was constructed for hosted-agent-endpoint agents. FoundryAgent agent = new(s_testAgentEndpoint, new FakeAuthenticationTokenProvider()); Assert.NotNull(agent.GetService()); @@ -450,6 +462,7 @@ public class FoundryAgentTests [Fact] public void ProjectEndpointConstructor_GetServiceProjectOpenAIClient_ReturnsNull() { + // See AgentEndpointConstructor_GetServiceProjectOpenAIClient_ReturnsNull for rationale. FoundryAgent agent = new( s_testEndpoint, new FakeAuthenticationTokenProvider(), @@ -611,6 +624,57 @@ public class FoundryAgentTests Assert.True(meaiSeen, "Expected MEAI/x.y.z to appear in the User-Agent header on the agent-endpoint pipeline."); } + [Fact] + public void AgentEndpointConstructor_ExposesFoundryProviderName_OnChatClientMetadata() + { + // Behavior change: after the FoundryChatClient consolidation, the agent-endpoint path + // now wraps with FoundryChatClient in the Agent Endpoint mode (Mode 3) and stamps the microsoft.foundry provider + // name. Previously this path used a bare AsIChatClient() with no Foundry-specific + // decorator, so the provider name defaulted to whatever MEAI surfaces. This guards the + // new behavior. + FoundryAgent agent = new(s_testAgentEndpoint, new FakeAuthenticationTokenProvider()); + + var metadata = agent.GetService(); + Assert.NotNull(metadata); + Assert.Equal("microsoft.foundry", metadata!.ProviderName); + } + + [Fact] + public async Task AgentEndpointConstructor_StampsAgentFrameworkUserAgentSegmentAsync() + { + // Behavior change: after the FoundryChatClient consolidation, every outbound request + // from the agent-endpoint constructor carries the agent-framework-dotnet/{version} + // segment via AgentFrameworkUserAgentPolicy. Previously this path had no + // agent-framework branding at all. + bool afSeen = false; + using HttpHandlerAssert handler = new(req => + { + if (req.Headers.TryGetValues("User-Agent", out var values)) + { + foreach (string v in values) + { + if (v.Contains("agent-framework-dotnet/")) + { + afSeen = true; + } + } + } + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json"), + }; + }); +#pragma warning disable CA5399 + using HttpClient http = new(handler); +#pragma warning restore CA5399 + ProjectOpenAIClientOptions opts = new() { Transport = new HttpClientPipelineTransport(http) }; + + FoundryAgent agent = new(s_testAgentEndpoint, new FakeAuthenticationTokenProvider(), clientOptions: opts); + await agent.RunAsync("Hello"); + + Assert.True(afSeen, "Expected agent-framework-dotnet/{version} segment on the agent-endpoint outbound User-Agent."); + } + [Fact] public async Task AgentEndpointConstructor_PassesThroughCallerPolicyOnPerAgentPipelineAsync() { @@ -666,82 +730,22 @@ public class FoundryAgentTests } [Fact] - public void AgentEndpointConstructor_PreservesUserAgentApplicationId() + public void AgentEndpointConstructor_PropagatesUserAgentApplicationId_ToProjectLevelClient() { + // The MEAI policy adds its own User-Agent header so we cannot reliably observe the OpenAI SDK's + // application-id stamp in the outbound request. Verify the value is propagated onto the + // caller's options bag and that the materialized AIProjectClient is reachable so + // downstream conversation/file/vector-store operations can pick the application id up. ProjectOpenAIClientOptions opts = new() { UserAgentApplicationId = "my-app-id" }; FoundryAgent agent = new(s_testAgentEndpoint, new FakeAuthenticationTokenProvider(), clientOptions: opts); + AIProjectClient? aiProjectClient = agent.GetService(); + Assert.NotNull(aiProjectClient); // Caller's UserAgentApplicationId is preserved on the per-agent options bag verbatim. - Assert.NotNull(agent); Assert.Equal("my-app-id", opts.UserAgentApplicationId); } - [Fact] - public void CreateProjectClientOptions_NullCallerOptions_ReturnsNull() - { - Assert.Null(FoundryAgent.CreateProjectClientOptions(null)); - } - - [Fact] - public void CreateProjectClientOptions_CarriesPipelineSettingsAndUserAgent() - { - // Arrange - var transport = new FakePipelineTransport(); - var retryPolicy = new FakeRetryPolicy(); - var messageLoggingPolicy = new FakeMessageLoggingPolicy(); - var clientLoggingOptions = new ClientLoggingOptions { EnableLogging = false }; - var networkTimeout = TimeSpan.FromSeconds(42); - - ProjectOpenAIClientOptions callerOptions = new() - { - UserAgentApplicationId = "my-app-id", - Transport = transport, - RetryPolicy = retryPolicy, - MessageLoggingPolicy = messageLoggingPolicy, - ClientLoggingOptions = clientLoggingOptions, - NetworkTimeout = networkTimeout, - }; - - // Act - AIProjectClientOptions? projectOptions = FoundryAgent.CreateProjectClientOptions(callerOptions); - - // Assert: every settable pipeline behavior the caller configured is forwarded - // onto the project-level options bag, not silently dropped. - Assert.NotNull(projectOptions); - Assert.Equal("my-app-id", projectOptions!.UserAgentApplicationId); - Assert.Same(transport, projectOptions.Transport); - Assert.Same(retryPolicy, projectOptions.RetryPolicy); - Assert.Same(messageLoggingPolicy, projectOptions.MessageLoggingPolicy); - Assert.Same(clientLoggingOptions, projectOptions.ClientLoggingOptions); - Assert.Equal(networkTimeout, projectOptions.NetworkTimeout); - } - - private sealed class FakeRetryPolicy : PipelinePolicy - { - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - => ProcessNext(message, pipeline, currentIndex); - - public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - => ProcessNextAsync(message, pipeline, currentIndex); - } - - private sealed class FakeMessageLoggingPolicy : PipelinePolicy - { - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - => ProcessNext(message, pipeline, currentIndex); - - public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - => ProcessNextAsync(message, pipeline, currentIndex); - } - - private sealed class FakePipelineTransport : PipelineTransport - { - protected override PipelineMessage CreateMessageCore() => throw new NotSupportedException(); - protected override void ProcessCore(PipelineMessage message) => throw new NotSupportedException(); - protected override ValueTask ProcessCoreAsync(PipelineMessage message) => throw new NotSupportedException(); - } - #endregion #region ParseAgentEndpoint tests @@ -824,13 +828,13 @@ public class FoundryAgentTests private readonly string _value; public HeaderStampPolicy(string name, string value) { this._name = name; this._value = value; } - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + public override void Process(PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { message.Request.Headers.Set(this._name, this._value); ProcessNext(message, pipeline, currentIndex); } - public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + public override ValueTask ProcessAsync(PipelineMessage message, System.Collections.Generic.IReadOnlyList pipeline, int currentIndex) { message.Request.Headers.Set(this._name, this._value); return ProcessNextAsync(message, pipeline, currentIndex); diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientTests.cs new file mode 100644 index 0000000000..f075d80857 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientTests.cs @@ -0,0 +1,616 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel.Primitives; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Microsoft.Extensions.AI; + +#pragma warning disable OPENAI001, CS0618 + +namespace Microsoft.Agents.AI.Foundry.UnitTests; + +/// +/// Unit tests for the internal . Covers the three construction +/// modes (Responses Agent, Prompt Agent, Agent Endpoint), the GetService +/// returns per mode, the metadata-tagging contract, the agent-framework user-agent registration, +/// the Agent Endpoint mode (Mode 3) URL parsing happy and error paths, and end-to-end behavior through the public +/// AsAIAgent(AgentReference) extension that constructs a FoundryChatClient internally. +/// +public sealed class FoundryChatClientTests +{ + #region the Responses Agent mode (Mode 1): Responses Agent (AIProjectClient + modelId) + + [Fact] + public void Mode1_ResponsesAgent_StampsFoundryProviderName() + { + // Arrange + var projectClient = CreateProjectClient(); + + // Act + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + // Assert + var metadata = chatClient.GetService(); + Assert.NotNull(metadata); + Assert.Equal("microsoft.foundry", metadata!.ProviderName); + Assert.Equal("gpt-4o-mini", metadata.DefaultModelId); + } + + [Fact] + public void Mode1_ResponsesAgent_ExposesAIProjectClient_ViaGetService() + { + // Arrange + var projectClient = CreateProjectClient(); + + // Act + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + // Assert + Assert.Same(projectClient, chatClient.GetService()); + // ProjectOpenAIClient is intentionally NOT exposed via GetService — callers retrieve + // it from the AIProjectClient themselves (aiProjectClient.GetProjectOpenAIClient()). + Assert.Null(chatClient.GetService()); + } + + [Fact] + public void Mode1_ResponsesAgent_ReturnsNullForAgentSpecificServices() + { + // Arrange + var projectClient = CreateProjectClient(); + + // Act + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + // Assert + Assert.Null(chatClient.GetService()); + Assert.Null(chatClient.GetService()); + Assert.Null(chatClient.GetService()); + // No agent name exists in the Responses Agent mode (Mode 1) — only the Prompt Agent mode (Mode 2) (from AgentReference.Name) and the Agent Endpoint mode (Mode 3) + // (parsed from URL) populate FoundryChatClient.AgentName. + Assert.Null(chatClient.AgentName); + } + + [Fact] + public void Mode1_ResponsesAgent_ThrowsOnNullProjectClient() + => Assert.Throws(() => new FoundryChatClient(aiProjectClient: null!, "gpt-4o-mini")); + + [Fact] + public void Mode1_ResponsesAgent_ThrowsOnEmptyModelId() + => Assert.Throws(() => new FoundryChatClient(CreateProjectClient(), modelId: "")); + + #endregion + + #region the Prompt Agent mode (Mode 2): Prompt Agent (direct unit tests) + + [Fact] + public void Mode2_PromptAgent_StampsFoundryProviderNameAndDefaultModelId() + { + // Arrange + var projectClient = CreateProjectClient(); + var agentRef = new AgentReference("agent-name", "1"); + + // Act + var chatClient = new FoundryChatClient(projectClient, agentRef, defaultModelId: "gpt-4o", baseChatOptions: null); + + // Assert + var metadata = chatClient.GetService(); + Assert.NotNull(metadata); + Assert.Equal("microsoft.foundry", metadata!.ProviderName); + Assert.Equal("gpt-4o", metadata.DefaultModelId); + } + + [Fact] + public void Mode2_PromptAgent_ExposesAgentReference_ViaGetService() + { + // Arrange + var projectClient = CreateProjectClient(); + var agentRef = new AgentReference("agent-name", "1"); + + // Act + var chatClient = new FoundryChatClient(projectClient, agentRef, defaultModelId: null, baseChatOptions: null); + + // Assert + Assert.Same(agentRef, chatClient.GetService()); + Assert.Same(projectClient, chatClient.GetService()); + // ProjectOpenAIClient is intentionally NOT exposed via GetService — see comment in + // Mode1_ResponsesAgent_ExposesAIProjectClient_ViaGetService. + Assert.Null(chatClient.GetService()); + // Version/Record were not provided via this ctor. + Assert.Null(chatClient.GetService()); + Assert.Null(chatClient.GetService()); + } + + [Fact] + public void Mode2_PromptAgent_PopulatesAgentNameFromAgentReference() + { + // Arrange + var projectClient = CreateProjectClient(); + var agentRef = new AgentReference("my-server-side-agent", "1"); + + // Act + var chatClient = new FoundryChatClient(projectClient, agentRef, defaultModelId: null, baseChatOptions: null); + + // Assert: AgentName is general-purpose across the Prompt Agent (Mode 2) and Agent Endpoint (Mode 3) modes. In the Prompt Agent mode (Mode 2) it mirrors + // AgentReference.Name so callers have a uniform handle regardless of construction mode. + Assert.Equal("my-server-side-agent", chatClient.AgentName); + } + + [Fact] + public void Mode2_PromptAgent_AllowsNullDefaultModelIdAndBaseChatOptions() + { + // Arrange + var projectClient = CreateProjectClient(); + var agentRef = new AgentReference("agent-name", "1"); + + // Act + Assert: must not throw; defaultModelId and baseChatOptions are optional. + var chatClient = new FoundryChatClient(projectClient, agentRef, defaultModelId: null, baseChatOptions: null); + Assert.NotNull(chatClient); + } + + [Fact] + public void Mode2_PromptAgent_ThrowsOnNullAgentReference() + => Assert.Throws(() => + new FoundryChatClient(CreateProjectClient(), agentReference: null!, defaultModelId: null, baseChatOptions: null)); + + #endregion + + #region the Prompt Agent mode (Mode 2): Prompt Agent end-to-end round-trip via AsAIAgent(AgentReference) extension + + // The end-to-end tests below exercise the same FoundryChatClient mode-2 behaviors above, + // but through the public AsAIAgent(AgentReference) extension that constructs a FoundryChatClient + // internally. They focus on the conversation-id handling that only manifests through the + // ChatClientAgentSession surface, which requires a fully assembled agent rather than a bare + // chat client. + + /// + /// Verify that after the first RunAsync, the session's ConversationId is set from the + /// response, and subsequent requests include that conversation ID automatically. + /// + [Fact] + public async Task EndToEnd_AgentReference_UsesDefaultConversationIdAsync() + { + // Arrange + var responsesRequestCount = 0; + using var httpHandler = new HttpHandlerAssert(async (request) => + { + if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) + { + responsesRequestCount++; + + // Assert: On the second Responses API call, verify the conversation ID + // from the first response is automatically included in the request body. + if (responsesRequestCount == 2 && request.Content is not null) + { + var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); + Assert.Contains("resp_0888a", requestBody); + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(httpHandler); +#pragma warning restore CA5399 + + AIProjectClient projectClient = new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); + + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); + + // Act + var session = await agent.CreateSessionAsync(); + await agent.RunAsync("Hello", session); + await agent.RunAsync("Follow up", session); + + // Assert + Assert.Equal(2, responsesRequestCount); + var chatClientSession = Assert.IsType(session); + Assert.Equal("resp_0888a46cbf2b1ff3006914596e05d08195a77c3f5187b769a7", chatClientSession.ConversationId); + } + + /// + /// Verify that when the chat client doesn't have a default "conv_" conversation id, the chat client still uses the conversation ID in HTTP requests. + /// + [Fact] + public async Task EndToEnd_AgentReference_UsesPerRequestConversationId_WhenNoDefaultConversationIdIsProvidedAsync() + { + // Arrange + var requestTriggered = false; + using var httpHandler = new HttpHandlerAssert(async (request) => + { + if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) + { + requestTriggered = true; + + // Assert + if (request.Content is not null) + { + var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); + Assert.Contains("conv_12345", requestBody); + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(httpHandler); +#pragma warning restore CA5399 + + AIProjectClient projectClient = new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); + + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); + + // Act + var session = await agent.CreateSessionAsync(); + await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } }); + + Assert.True(requestTriggered); + var chatClientSession = Assert.IsType(session); + Assert.Equal("conv_12345", chatClientSession.ConversationId); + } + + /// + /// Verify that even when the chat client has a default conversation id, the chat client will prioritize the per-request conversation id provided in HTTP requests. + /// + [Fact] + public async Task EndToEnd_AgentReference_UsesPerRequestConversationId_EvenWhenDefaultConversationIdIsProvidedAsync() + { + // Arrange + var requestTriggered = false; + using var httpHandler = new HttpHandlerAssert(async (request) => + { + if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) + { + requestTriggered = true; + + // Assert + if (request.Content is not null) + { + var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); + Assert.Contains("conv_12345", requestBody); + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(httpHandler); +#pragma warning restore CA5399 + + AIProjectClient projectClient = new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); + + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); + + // Act + var session = await agent.CreateSessionAsync(); + await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "conv_12345" } }); + + Assert.True(requestTriggered); + var chatClientSession = Assert.IsType(session); + Assert.Equal("conv_12345", chatClientSession.ConversationId); + } + + /// + /// Verify that when the chat client is provided without a "conv_" prefixed conversation ID, the chat client uses the previous conversation ID in HTTP requests. + /// + [Fact] + public async Task EndToEnd_AgentReference_UsesPreviousResponseId_WhenConversationIsNotPrefixedAsConvAsync() + { + // Arrange + var requestTriggered = false; + using var httpHandler = new HttpHandlerAssert(async (request) => + { + if (request.Method == HttpMethod.Post && request.RequestUri!.PathAndQuery.Contains("/responses")) + { + requestTriggered = true; + + // Assert + if (request.Content is not null) + { + var requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false); + Assert.Contains("resp_0888a", requestBody); + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetOpenAIDefaultResponseJson(), Encoding.UTF8, "application/json") }; + } + + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(TestDataUtil.GetAgentResponseJson(), Encoding.UTF8, "application/json") }; + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(httpHandler); +#pragma warning restore CA5399 + + AIProjectClient projectClient = new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions() { Transport = new HttpClientPipelineTransport(httpClient) }); + + var agent = projectClient.AsAIAgent(new AgentReference("agent-name")); + + // Act + var session = await agent.CreateSessionAsync(); + await agent.RunAsync("Hello", session, options: new ChatClientAgentRunOptions() { ChatOptions = new() { ConversationId = "resp_0888a" } }); + + Assert.True(requestTriggered); + var chatClientSession = Assert.IsType(session); + Assert.Equal("resp_0888a46cbf2b1ff3006914596e05d08195a77c3f5187b769a7", chatClientSession.ConversationId); + } + + #endregion + + #region the Agent Endpoint mode (Mode 3): Agent Endpoint + + [Fact] + public void Mode3_AgentEndpoint_ParsesAgentNameFromUrl() + { + // Arrange + Act + var chatClient = new FoundryChatClient( + agentEndpoint: new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"), + credential: new FakeAuthenticationTokenProvider(), + clientOptions: null); + + // Assert + Assert.Equal("myagent", chatClient.AgentName); + } + + [Fact] + public void Mode3_AgentEndpoint_StampsFoundryProviderName() + { + // Act + var chatClient = new FoundryChatClient( + agentEndpoint: new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"), + credential: new FakeAuthenticationTokenProvider(), + clientOptions: null); + + // Assert + var metadata = chatClient.GetService(); + Assert.NotNull(metadata); + Assert.Equal("microsoft.foundry", metadata!.ProviderName); + // No model id is knowable from the URL alone. + Assert.Null(metadata.DefaultModelId); + } + + [Fact] + public void Mode3_AgentEndpoint_ExposesProjectOpenAIClientAndAIProjectClient() + { + // Act + var chatClient = new FoundryChatClient( + agentEndpoint: new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"), + credential: new FakeAuthenticationTokenProvider(), + clientOptions: null); + + // Assert + // ProjectOpenAIClient is intentionally NOT exposed via GetService — callers retrieve + // it from the AIProjectClient themselves (aiProjectClient.GetProjectOpenAIClient()). + Assert.Null(chatClient.GetService()); + // After the materialization change, the Agent Endpoint mode (Mode 3) also exposes a working AIProjectClient + // built from the parsed project root. This makes the helper surface symmetric across + // all three construction modes. + Assert.NotNull(chatClient.GetService()); + Assert.Null(chatClient.GetService()); + Assert.Null(chatClient.GetService()); + Assert.Null(chatClient.GetService()); + } + + [Fact] + public void Mode3_AgentEndpoint_MaterializedAIProjectClient_TargetsParsedProjectRoot() + { + // The Agent Endpoint mode (Mode 3) ctor must derive the project root from the agent endpoint URL and + // construct the AIProjectClient against that root, NOT the agent endpoint itself. + var agentEndpoint = new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"); + var chatClient = new FoundryChatClient( + agentEndpoint: agentEndpoint, + credential: new FakeAuthenticationTokenProvider(), + clientOptions: null); + + var aiProjectClient = chatClient.GetService(); + Assert.NotNull(aiProjectClient); + // AIProjectClient does not expose its endpoint publicly, so we rely on reflection on + // the well-known private field. If the SDK field shape changes this guard fails loudly. + var field = typeof(AIProjectClient).GetField("_endpoint", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(field); + var actualEndpoint = (Uri)field!.GetValue(aiProjectClient!)!; + Assert.Equal("https://example.com/api/projects/myproj", actualEndpoint.AbsoluteUri.TrimEnd('/')); + } + + [Fact] + public void Mode3_AgentEndpoint_MaterializedAIProjectClient_IsReusedAcrossGetServiceCalls() + { + // Repeated GetService() calls must return the same instance — the + // materialized client is cached in the existing _aiProjectClient field, not built on + // demand each call. + var chatClient = new FoundryChatClient( + agentEndpoint: new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"), + credential: new FakeAuthenticationTokenProvider(), + clientOptions: null); + + var first = chatClient.GetService(); + var second = chatClient.GetService(); + Assert.NotNull(first); + Assert.Same(first, second); + } + + [Fact] + public void Mode1_ResponsesAgent_AIProjectClient_IsTheSuppliedInstance() + { + // Regression check: the Responses Agent mode (Mode 1) must continue to expose the AIProjectClient the caller + // supplied via the constructor, NOT a freshly-materialized one. + var supplied = CreateProjectClient(); + var chatClient = new FoundryChatClient(supplied, "gpt-4o-mini"); + Assert.Same(supplied, chatClient.GetService()); + } + + [Fact] + public void Mode2_PromptAgent_AIProjectClient_IsTheSuppliedInstance() + { + // Regression check: the Prompt Agent mode (Mode 2) must continue to expose the AIProjectClient the caller + // supplied via the constructor. + var supplied = CreateProjectClient(); + var agentRef = new AgentReference("agent-name", "1"); + var chatClient = new FoundryChatClient(supplied, agentRef, defaultModelId: null, baseChatOptions: null); + Assert.Same(supplied, chatClient.GetService()); + } + + [Fact] + public void Mode3_AgentEndpoint_ThrowsOnNullEndpoint() + => Assert.Throws(() => + new FoundryChatClient(agentEndpoint: null!, credential: new FakeAuthenticationTokenProvider(), clientOptions: null)); + + [Fact] + public void Mode3_AgentEndpoint_ThrowsOnNullCredential() + => Assert.Throws(() => + new FoundryChatClient( + agentEndpoint: new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"), + credential: null!, + clientOptions: null)); + + #endregion + + #region ParseAgentEndpoint URL parsing + + [Fact] + public void ParseAgentEndpoint_HappyPath_ReturnsAgentNameAndProjectRoot() + { + // Act + var (agentName, projectRoot) = FoundryChatClient.ParseAgentEndpoint( + new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai")); + + // Assert + Assert.Equal("myagent", agentName); + Assert.Equal("https://example.com/api/projects/myproj", projectRoot.AbsoluteUri.TrimEnd('/')); + } + + [Fact] + public void ParseAgentEndpoint_TolerantOfTrailingSlash() + { + // Act + var (agentName, _) = FoundryChatClient.ParseAgentEndpoint( + new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai/")); + + // Assert + Assert.Equal("myagent", agentName); + } + + [Fact] + public void ParseAgentEndpoint_TolerantOfCaseDifferencesOnAgentsSegment() + { + // Act + var (agentName, _) = FoundryChatClient.ParseAgentEndpoint( + new Uri("https://example.com/api/projects/myproj/AGENTS/myagent/endpoint/protocols/openai")); + + // Assert + Assert.Equal("myagent", agentName); + } + + [Fact] + public void ParseAgentEndpoint_StripsQueryAndFragment() + { + // Act + var (_, projectRoot) = FoundryChatClient.ParseAgentEndpoint( + new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai?api-version=v1#frag")); + + // Assert + Assert.Equal(string.Empty, projectRoot.Query); + Assert.Equal(string.Empty, projectRoot.Fragment); + } + + [Fact] + public void ParseAgentEndpoint_ThrowsOnMissingAgentsSegment() + => Assert.Throws(() => + FoundryChatClient.ParseAgentEndpoint(new Uri("https://example.com/api/projects/myproj/anyseg/myagent/endpoint/protocols/openai"))); + + [Fact] + public void ParseAgentEndpoint_ThrowsOnWrongSuffix() + => Assert.Throws(() => + FoundryChatClient.ParseAgentEndpoint(new Uri("https://example.com/api/projects/myproj/agents/myagent/wrong/suffix"))); + + [Fact] + public void ParseAgentEndpoint_ThrowsOnNullUri() + => Assert.Throws(() => FoundryChatClient.ParseAgentEndpoint(null!)); + + #endregion + + #region AgentFrameworkUserAgentPolicy registration + dedup + + [Fact] + public void Register_AgentFrameworkUserAgentPolicy_OnUnderlyingOpenAIRequestPolicies() + { + // Arrange + Act: constructing a FoundryChatClient should register the + // AgentFrameworkUserAgentPolicy on the inner chat client's OpenAIRequestPolicies. + var chatClient = new FoundryChatClient(CreateProjectClient(), "gpt-4o-mini"); + + // Assert: the inner chat client (MEAI's OpenAIResponsesChatClient) exposes + // OpenAIRequestPolicies via GetService, and our policy is present in its entries. + var policies = chatClient.GetService(); + Assert.NotNull(policies); + Assert.Equal(1, EntriesCount(policies!)); + } + + [Fact] + public void Register_AgentFrameworkUserAgentPolicy_IsDedupedAcrossMultipleClients_OnSharedInner() + { + // Arrange: construct via the ProjectsAgentVersion mode-2 variant, which chains via + // :this(...) into the AgentReference ctor. If the policy registration code were + // inadvertently called twice along the chain, we would see 2 entries. + var projectClient = CreateProjectClient(); + var agentVersion = ModelReaderWriter.Read( + BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!; + + // Act + var chatClient = new FoundryChatClient(projectClient, agentVersion, baseChatOptions: null); + + // Assert: even though the version variant funnels through the AgentReference ctor + // via :this(...), the policy is registered exactly once on the inner pipeline. + var policies = chatClient.GetService(); + Assert.NotNull(policies); + Assert.Equal(1, EntriesCount(policies!)); + Assert.Same(agentVersion, chatClient.GetService()); + Assert.NotNull(chatClient.GetService()); + } + + #endregion + + #region Helpers + + private static AIProjectClient CreateProjectClient() + => new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(new HttpClient()) }); + + private static int EntriesCount(OpenAIRequestPolicies policies) + { + var field = typeof(OpenAIRequestPolicies).GetField("_entries", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(field); + var arr = (Array)field!.GetValue(policies)!; + return arr.Length; + } + + #endregion +} +#pragma warning restore CS0618 diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientVectorStoreTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientVectorStoreTests.cs new file mode 100644 index 0000000000..923d615807 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryChatClientVectorStoreTests.cs @@ -0,0 +1,660 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using OpenAI.Files; + +#pragma warning disable OPENAI001, CS0618 + +namespace Microsoft.Agents.AI.Foundry.UnitTests; + +/// +/// Unit tests for the file and vector-store helper methods on . +/// Covers all four methods across the three FoundryChatClient construction modes plus argument +/// validation, cancellation, and request-body shape on the wire. +/// +public sealed class FoundryChatClientVectorStoreTests +{ + // ----- Construction helpers shared by every test in this file ----- + + private static (FoundryChatClient ChatClient, RequestRecorder Recorder) CreateMode1(string modelId = "gpt-4o-mini", string? responseBody = null) + { + var recorder = new RequestRecorder(responseBody); +#pragma warning disable CA5399 + var httpClient = new HttpClient(recorder); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + return (new FoundryChatClient(projectClient, modelId), recorder); + } + + private static (FoundryChatClient ChatClient, RequestRecorder Recorder) CreateMode2(string? responseBody = null) + { + var recorder = new RequestRecorder(responseBody); +#pragma warning disable CA5399 + var httpClient = new HttpClient(recorder); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var agentRef = new AgentReference("agent-name", "1"); + return (new FoundryChatClient(projectClient, agentRef, defaultModelId: "gpt-4o", baseChatOptions: null), recorder); + } + + private static string MakeTempFile(string contents = "hello world") + { + var path = Path.Combine(Path.GetTempPath(), $"fcc-test-{Guid.NewGuid():N}.txt"); + File.WriteAllText(path, contents); + return path; + } + + // ----- UploadFileAsync ----- + + [Fact] + public async Task UploadFileAsync_Mode1_UploadsViaProjectOpenAIClientAsync() + { + var (chatClient, recorder) = CreateMode1(responseBody: FakeFileJson("file_abc")); + var path = MakeTempFile(); + try + { + var result = await chatClient.UploadFileAsync(path, FileUploadPurpose.Assistants); + + Assert.Equal("file_abc", result.Id); + Assert.NotEmpty(recorder.Requests); + Assert.EndsWith("/files", recorder.Requests[0].PathAndQuery.TrimEnd('/').Split('?')[0]); + } + finally { File.Delete(path); } + } + + [Fact] + public async Task UploadFileAsync_Mode2_UploadsViaProjectOpenAIClientAsync() + { + var (chatClient, recorder) = CreateMode2(responseBody: FakeFileJson("file_xyz")); + var path = MakeTempFile(); + try + { + var result = await chatClient.UploadFileAsync(path, FileUploadPurpose.Assistants); + Assert.Equal("file_xyz", result.Id); + Assert.Contains(recorder.Requests, r => r.PathAndQuery.Contains("/files")); + } + finally { File.Delete(path); } + } + + [Fact] + public async Task UploadFileAsync_Mode3_UploadsViaMaterializedProjectClientAsync() + { + // Q-E: Mode 3 (Agent Endpoint) now honors caller-supplied transports via + // ProjectOpenAIClientOptions.Transport, so we can use a fake transport here instead of + // depending on DNS/network availability against example.com. + var sawUpload = false; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Post && req.RequestUri!.AbsolutePath.Contains("/files", StringComparison.Ordinal)) + { + sawUpload = true; + return MakeJsonResponse(FakeFileJson("file_mode3")); + } + return MakeJsonResponse("{}"); + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var chatClient = new FoundryChatClient( + agentEndpoint: new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"), + credential: new FakeAuthenticationTokenProvider(), + clientOptions: new ProjectOpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + + var path = MakeTempFile(); + try + { + var result = await chatClient.UploadFileAsync(path, FileUploadPurpose.Assistants, CancellationToken.None); + Assert.True(sawUpload); + Assert.Equal("file_mode3", result.Id); + } + finally { File.Delete(path); } + } + + [Fact] + public async Task UploadFileAsync_NullFilePath_ThrowsArgumentNullExceptionAsync() + { + var (chatClient, _) = CreateMode1(); + await Assert.ThrowsAsync(() => + chatClient.UploadFileAsync(null!, FileUploadPurpose.Assistants)); + } + + [Fact] + public async Task UploadFileAsync_FileNotFound_ThrowsFileNotFoundExceptionAsync() + { + var (chatClient, _) = CreateMode1(); + var missing = Path.Combine(Path.GetTempPath(), $"does-not-exist-{Guid.NewGuid():N}.txt"); + await Assert.ThrowsAsync(() => + chatClient.UploadFileAsync(missing, FileUploadPurpose.Assistants)); + } + + [Fact] + public async Task UploadFileAsync_HonorsCancellationAsync() + { + // Cancellation propagation through the OpenAI SDK pipeline surfaces different exception + // types depending on the framework target (OperationCanceledException on net10.0, + // ObjectDisposedException at the transport layer on net472). Asserting on the exact + // exception class is brittle; assert only that the call throws when the token is + // pre-cancelled. + var (chatClient, _) = CreateMode1(responseBody: FakeFileJson("file_abc")); + var path = MakeTempFile(); + try + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => + chatClient.UploadFileAsync(path, FileUploadPurpose.Assistants, cts.Token)); + } + finally { File.Delete(path); } + } + + // ----- DeleteFileAsync ----- + + [Fact] + public async Task DeleteFileAsync_Mode1_CallsDeleteOnFileClientAsync() + { + var (chatClient, recorder) = CreateMode1(responseBody: FakeFileDeletedJson("file_abc")); + await chatClient.DeleteFileAsync("file_abc"); + Assert.Contains(recorder.Requests, r => r.Method == "DELETE" && r.PathAndQuery.Contains("/files/file_abc")); + } + + [Fact] + public async Task DeleteFileAsync_Mode2_CallsDeleteOnFileClientAsync() + { + var (chatClient, recorder) = CreateMode2(responseBody: FakeFileDeletedJson("file_xyz")); + await chatClient.DeleteFileAsync("file_xyz"); + Assert.Contains(recorder.Requests, r => r.Method == "DELETE" && r.PathAndQuery.Contains("/files/file_xyz")); + } + + [Fact] + public async Task DeleteFileAsync_NullId_ThrowsArgumentExceptionAsync() + { + var (chatClient, _) = CreateMode1(); + await Assert.ThrowsAnyAsync(() => chatClient.DeleteFileAsync(null!)); + } + + [Fact] + public async Task DeleteFileAsync_EmptyId_ThrowsArgumentExceptionAsync() + { + var (chatClient, _) = CreateMode1(); + await Assert.ThrowsAnyAsync(() => chatClient.DeleteFileAsync("")); + } + + [Fact] + public async Task DeleteFileAsync_HonorsCancellationAsync() + { + // Verify the cancellation token reaches the HTTP pipeline by having the handler + // throw OperationCanceledException when the token is cancelled before the request. + // This is more robust than asserting on the exact exception the SDK surfaces, which + // depends on internal pipeline plumbing. + var observedToken = CancellationToken.None; + using var handler = new HttpHandlerAssert(async req => + { + // We don't have direct access to the SDK's CancellationToken here; instead, sleep + // briefly to give the caller's pre-cancellation a chance to be picked up by the + // transport. If cancellation reached the pipeline, the await on this handler call + // would surface OperationCanceledException; if not, the response is returned. + await Task.Delay(50).ConfigureAwait(false); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(FakeFileDeletedJson("file_abc"), Encoding.UTF8, "application/json"), + }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); + // Any throw is acceptable evidence that cancellation was honored. The SDK's exact + // exception surface for pre-cancelled tokens is an implementation detail of + // System.ClientModel's pipeline and may differ between versions. + await Assert.ThrowsAnyAsync(() => chatClient.DeleteFileAsync("file_abc", cts.Token)); + } + + // ----- CreateVectorStoreAsync ----- + + [Fact] + public async Task CreateVectorStoreAsync_UploadsThenCreates_WithFileIds_ReturnsVectorStoreAsync() + { + // Each file POST returns a distinct file id; the recorder dispatches on URL to differentiate. + var fileCount = 0; + using var handler = new HttpHandlerAssert(async req => + { + var body = req.Content is null ? "" : await req.Content.ReadAsStringAsync().ConfigureAwait(false); + if (req.RequestUri!.AbsolutePath.Contains("/files") && req.Method == HttpMethod.Post) + { + fileCount++; + return MakeJsonResponse(FakeFileJson($"file_{fileCount}")); + } + if (req.RequestUri.AbsolutePath.Contains("/vector_stores") && req.Method == HttpMethod.Post) + { + Assert.Contains("file_1", body); + Assert.Contains("file_2", body); + Assert.Contains("knowledge-base", body); + return MakeJsonResponse(FakeVectorStoreJson("vs_abc", name: "knowledge-base")); + } + return MakeJsonResponse("{}"); + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + var pathA = MakeTempFile("alpha"); + var pathB = MakeTempFile("beta"); + try + { + var store = await chatClient.CreateVectorStoreAsync("knowledge-base", new[] { pathA, pathB }); + Assert.Equal("vs_abc", store.Id); + Assert.Equal(2, fileCount); + } + finally { File.Delete(pathA); File.Delete(pathB); } + } + + [Fact] + public async Task CreateVectorStoreAsync_WithExpiresAfter_SerializesLastActiveAtAnchorAsync() + { + string? vectorStoreBody = null; + using var handler = new HttpHandlerAssert(async req => + { + if (req.RequestUri!.AbsolutePath.Contains("/vector_stores") && req.Method == HttpMethod.Post) + { + vectorStoreBody = req.Content is null ? "" : await req.Content.ReadAsStringAsync().ConfigureAwait(false); + return MakeJsonResponse(FakeVectorStoreJson("vs_abc", name: "x")); + } + return MakeJsonResponse("{}"); + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + await chatClient.CreateVectorStoreAsync("x", Array.Empty(), expiresAfter: TimeSpan.FromDays(7)); + + Assert.NotNull(vectorStoreBody); + Assert.Contains("\"expires_after\"", vectorStoreBody); + Assert.Contains("\"last_active_at\"", vectorStoreBody); + Assert.Contains("\"days\":7", vectorStoreBody); + } + + [Fact] + public async Task CreateVectorStoreAsync_WithNullExpiresAfter_OmitsExpirationPolicyAsync() + { + string? vectorStoreBody = null; + using var handler = new HttpHandlerAssert(async req => + { + if (req.RequestUri!.AbsolutePath.Contains("/vector_stores") && req.Method == HttpMethod.Post) + { + vectorStoreBody = req.Content is null ? "" : await req.Content.ReadAsStringAsync().ConfigureAwait(false); + return MakeJsonResponse(FakeVectorStoreJson("vs_abc", name: "x")); + } + return MakeJsonResponse("{}"); + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + await chatClient.CreateVectorStoreAsync("x", Array.Empty(), expiresAfter: null); + + Assert.NotNull(vectorStoreBody); + Assert.DoesNotContain("\"expires_after\"", vectorStoreBody); + } + + [Fact] + public async Task CreateVectorStoreAsync_EmptyFilesList_CreatesEmptyStoreAsync() + { + var (chatClient, _) = CreateMode1(responseBody: FakeVectorStoreJson("vs_empty", name: "x")); + var store = await chatClient.CreateVectorStoreAsync("x", Array.Empty()); + Assert.Equal("vs_empty", store.Id); + } + + [Fact] + public async Task CreateVectorStoreAsync_NullName_ThrowsArgumentExceptionAsync() + { + var (chatClient, _) = CreateMode1(); + await Assert.ThrowsAnyAsync(() => + chatClient.CreateVectorStoreAsync(null!, Array.Empty())); + } + + [Fact] + public async Task CreateVectorStoreAsync_NullFilePaths_ThrowsArgumentNullExceptionAsync() + { + var (chatClient, _) = CreateMode1(); + await Assert.ThrowsAsync(() => + chatClient.CreateVectorStoreAsync("x", filePaths: null!)); + } + + [Fact] + public async Task CreateVectorStoreAsync_HonorsCancellationAsync() + { + // Same rationale as UploadFileAsync_HonorsCancellationAsync — assert only that any + // exception is thrown on a pre-cancelled token. + var (chatClient, _) = CreateMode1(responseBody: FakeVectorStoreJson("vs_x", "x")); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => + chatClient.CreateVectorStoreAsync("x", Array.Empty(), expiresAfter: null, cancellationToken: cts.Token)); + } + + [Fact] + public async Task CreateVectorStoreAsync_PollsUntilStoreLeavesInProgress_Async() + { + // Q-A regression: when the create response returns status=in_progress, the helper must + // poll GET /vector_stores/{id} until status changes before returning. Otherwise the + // caller receives a half-built store. + var pollCount = 0; + using var handler = new HttpHandlerAssert(req => + { + if (req.RequestUri!.AbsolutePath.Contains("/vector_stores") && req.Method == HttpMethod.Post) + { + // First response: status=in_progress. + return Task.FromResult(MakeJsonResponse(FakeVectorStoreJsonWithStatus("vs_abc", name: "x", status: "in_progress"))); + } + if (req.RequestUri.AbsolutePath.Contains("/vector_stores/vs_abc") && req.Method == HttpMethod.Get) + { + pollCount++; + // Stay in_progress for two polls, then complete on the third. + var status = pollCount < 3 ? "in_progress" : "completed"; + return Task.FromResult(MakeJsonResponse(FakeVectorStoreJsonWithStatus("vs_abc", name: "x", status: status))); + } + return Task.FromResult(MakeJsonResponse("{}")); + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + var store = await chatClient.CreateVectorStoreAsync("x", Array.Empty()); + + Assert.NotEqual(OpenAI.VectorStores.VectorStoreStatus.InProgress, store.Status); + Assert.True(pollCount >= 3, $"Expected at least 3 GET polls before status leaves in_progress; saw {pollCount}."); + } + + [Fact] + public async Task CreateVectorStoreAsync_PollingTimeout_ThrowsTimeoutExceptionAsync() + { + // Sergey #2: caller-supplied (or default) polling timeout must surface as TimeoutException + // when the vector store never leaves InProgress. Mock keeps the store stuck and we pass + // a tiny timeout; cancellation token stays unused so the only path that ends the loop + // is the timeout check. + using var handler = new HttpHandlerAssert(req => + { + if (req.RequestUri!.AbsolutePath.Contains("/vector_stores", StringComparison.Ordinal)) + { + return Task.FromResult(MakeJsonResponse(FakeVectorStoreJsonWithStatus("vs_stuck", name: "x", status: "in_progress"))); + } + return Task.FromResult(MakeJsonResponse("{}")); + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + var ex = await Assert.ThrowsAsync(() => + chatClient.CreateVectorStoreAsync("x", Array.Empty(), expiresAfter: null, pollingTimeout: TimeSpan.FromMilliseconds(500))); + Assert.Contains("vs_stuck", ex.Message, StringComparison.Ordinal); + Assert.Contains("in-progress", ex.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task CreateVectorStoreAsync_MidUploadFailure_DeletesAlreadyUploadedFilesAsync() + { + // Q-B regression: when the upload loop throws partway through (e.g. file 3 of 5 is + // missing or the network fails), the helper must DELETE the already-uploaded files so + // they do not accumulate as orphaned resources. The exception must still propagate. + var uploadCount = 0; + var deleted = new List(); + using var handler = new HttpHandlerAssert(req => + { + // DELETE first so we don't match the upload-collection /files path against this. + if (req.Method == HttpMethod.Delete) + { + var segments = req.RequestUri!.AbsolutePath.Split('/'); + var fileId = segments[segments.Length - 1]; + deleted.Add(fileId); + return MakeJsonResponse(FakeFileDeletedJson(fileId)); + } + if (req.Method == HttpMethod.Post && req.RequestUri!.AbsolutePath.Contains("/files", StringComparison.Ordinal)) + { + uploadCount++; + if (uploadCount == 3) + { + // 400 is non-retriable; the SDK retry policy ignores it. 5xx would trigger + // retries and confuse the assertion on upload count. + return new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent("{\"error\":{\"code\":\"BadRequest\",\"message\":\"upload-failed-on-3\"}}", Encoding.UTF8, "application/json"), + }; + } + return MakeJsonResponse(FakeFileJson($"file_{uploadCount}")); + } + return MakeJsonResponse("{}"); + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + var paths = new[] { MakeTempFile("a"), MakeTempFile("b"), MakeTempFile("c"), MakeTempFile("d"), MakeTempFile("e") }; + try + { + await Assert.ThrowsAnyAsync(() => chatClient.CreateVectorStoreAsync("knowledge-base", paths)); + + // Three upload attempts: two succeeded, the third threw. + Assert.Equal(3, uploadCount); + // The two successful uploads must have been deleted as part of best-effort cleanup. + Assert.Equal(2, deleted.Count); + Assert.Contains("file_1", deleted); + Assert.Contains("file_2", deleted); + } + finally + { + foreach (var p in paths) + { + File.Delete(p); + } + } + } + + [Fact] + public async Task CreateVectorStoreAsync_MidUploadFailure_CleanupSwallowsDeleteErrorsAsync() + { + // Q-B follow-on: if a cleanup DELETE itself fails, the helper must still propagate the + // original upload exception — not the cleanup exception. The caller cares about the + // upload failure; cleanup is best-effort. + var uploadCount = 0; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Delete) + { + return new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent("{\"error\":{\"code\":\"DeleteFailed\",\"message\":\"cleanup-failed\"}}", Encoding.UTF8, "application/json"), + }; + } + if (req.Method == HttpMethod.Post && req.RequestUri!.AbsolutePath.Contains("/files", StringComparison.Ordinal)) + { + uploadCount++; + if (uploadCount == 2) + { + return new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent("{\"error\":{\"code\":\"BadRequest\",\"message\":\"upload-failed\"}}", Encoding.UTF8, "application/json"), + }; + } + return MakeJsonResponse(FakeFileJson($"file_{uploadCount}")); + } + return MakeJsonResponse("{}"); + }); + +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var chatClient = new FoundryChatClient(projectClient, "gpt-4o-mini"); + + var paths = new[] { MakeTempFile("a"), MakeTempFile("b") }; + try + { + var ex = await Assert.ThrowsAnyAsync(() => chatClient.CreateVectorStoreAsync("kb", paths)); + + // The original upload-failure message must surface, not the cleanup-failure message. + Assert.DoesNotContain("cleanup-failed", ex.Message ?? "", StringComparison.Ordinal); + } + finally + { + foreach (var p in paths) + { + File.Delete(p); + } + } + } + + // ----- DeleteVectorStoreAsync ----- + + [Fact] + public async Task DeleteVectorStoreAsync_Mode1_CallsDeleteAsync() + { + var (chatClient, recorder) = CreateMode1(responseBody: FakeVectorStoreDeletedJson("vs_abc")); + await chatClient.DeleteVectorStoreAsync("vs_abc"); + Assert.Contains(recorder.Requests, r => r.Method == "DELETE" && r.PathAndQuery.Contains("/vector_stores/vs_abc")); + } + + [Fact] + public async Task DeleteVectorStoreAsync_Mode2_CallsDeleteAsync() + { + var (chatClient, recorder) = CreateMode2(responseBody: FakeVectorStoreDeletedJson("vs_xyz")); + await chatClient.DeleteVectorStoreAsync("vs_xyz"); + Assert.Contains(recorder.Requests, r => r.Method == "DELETE" && r.PathAndQuery.Contains("/vector_stores/vs_xyz")); + } + + [Fact] + public async Task DeleteVectorStoreAsync_NullId_ThrowsArgumentExceptionAsync() + { + var (chatClient, _) = CreateMode1(); + await Assert.ThrowsAnyAsync(() => chatClient.DeleteVectorStoreAsync(null!)); + } + + [Fact] + public async Task DeleteVectorStoreAsync_HonorsCancellationAsync() + { + // Same approach as DeleteFileAsync_HonorsCancellationAsync — assert that the call + // throws when the token is pre-cancelled, without asserting on the exact exception + // surfaced by the SDK pipeline. + var (chatClient, _) = CreateMode1(responseBody: FakeVectorStoreDeletedJson("vs_abc")); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => chatClient.DeleteVectorStoreAsync("vs_abc", cts.Token)); + } + + // ----- Fixtures and helpers ----- + + private static HttpResponseMessage MakeJsonResponse(string json) + => new(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json"), + }; + + private static string FakeFileJson(string id) + => $"{{\"id\":\"{id}\",\"object\":\"file\",\"bytes\":11,\"created_at\":1700000000,\"filename\":\"x.txt\",\"purpose\":\"assistants\",\"status\":\"processed\"}}"; + + private static string FakeFileDeletedJson(string id) + => $"{{\"id\":\"{id}\",\"object\":\"file\",\"deleted\":true}}"; + + private static string FakeVectorStoreJson(string id, string name) + => FakeVectorStoreJsonWithStatus(id, name, status: "completed"); + + private static string FakeVectorStoreJsonWithStatus(string id, string name, string status) + => $"{{\"id\":\"{id}\",\"object\":\"vector_store\",\"created_at\":1700000000,\"name\":\"{name}\",\"usage_bytes\":0,\"file_counts\":{{\"in_progress\":0,\"completed\":0,\"failed\":0,\"cancelled\":0,\"total\":0}},\"status\":\"{status}\",\"last_active_at\":1700000000}}"; + + private static string FakeVectorStoreDeletedJson(string id) + => $"{{\"id\":\"{id}\",\"object\":\"vector_store.deleted\",\"deleted\":true}}"; + + private sealed class RequestRecorder : HttpClientHandler + { + private readonly string _responseBody; + public List Requests { get; } = []; + + public RequestRecorder(string? responseBody) + { + this._responseBody = responseBody ?? "{}"; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.Requests.Add(new RecordedRequest + { + Method = request.Method.Method, + PathAndQuery = request.RequestUri?.PathAndQuery ?? "", +#if NET + Body = request.Content is null ? "" : await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false), +#else + Body = request.Content is null ? "" : await request.Content.ReadAsStringAsync().ConfigureAwait(false), +#endif + }); + return MakeJsonResponse(this._responseBody); + } + } + + private sealed class RecordedRequest + { + public string Method { get; set; } = ""; + public string PathAndQuery { get; set; } = ""; + public string Body { get; set; } = ""; + } +} +#pragma warning restore CS0618 diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryPromptAgentConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryPromptAgentConverterTests.cs new file mode 100644 index 0000000000..6ba02029b1 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/FoundryPromptAgentConverterTests.cs @@ -0,0 +1,433 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel.Primitives; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.Extensions.OpenAI; +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Microsoft.Extensions.AI; +using OpenAI.Responses; + +#pragma warning disable OPENAI001, CS0618 + +namespace Microsoft.Agents.AI.Foundry.UnitTests; + +/// +/// Unit tests for the public ToPromptAgentAsync extension methods on +/// and . Both entry points dispatch +/// to the same internal converter, so each behavior is asserted through both surfaces. +/// +public sealed class FoundryPromptAgentConverterTests +{ + // ----- Failure modes (assert through ChatClientAgent and FoundryAgent extensions) ----- + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_NonFoundryChatClient_ThrowsInvalidOperationExceptionAsync() + { + var agent = new ChatClientAgent(new NoOpChatClient()); + var ex = await Assert.ThrowsAsync(() => agent.ToPromptAgentAsync()); + Assert.Contains("FoundryChatClient", ex.Message); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_FoundryChatClientInMode3_ThrowsInvalidOperationExceptionAsync() + { + var foundryAgent = new FoundryAgent( + agentEndpoint: new Uri("https://example.com/api/projects/myproj/agents/myagent/endpoint/protocols/openai"), + credential: new FakeAuthenticationTokenProvider()); + var ex = await Assert.ThrowsAsync(() => foundryAgent.ToPromptAgentAsync()); + Assert.Contains("Agent Endpoint mode (Mode 3)", ex.Message); + } + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_Mode1_MissingModelId_ThrowsInvalidOperationExceptionAsync() + { + var projectClient = CreateProjectClient(); + // Construct a FoundryChatClient via the Responses Agent mode (Mode 1) then wrap in a ChatClientAgent whose + // ChatOptions has no ModelId — synthesis must throw. + var fcc = new FoundryChatClient(projectClient, "gpt-4o-mini"); + var agent = new ChatClientAgent(fcc, new ChatClientAgentOptions { ChatOptions = new ChatOptions() }); + var ex = await Assert.ThrowsAsync(() => agent.ToPromptAgentAsync()); + Assert.Contains("model id", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_Mode1_UnsupportedAITool_ThrowsInvalidOperationExceptionNamingTypeAsync() + { + var projectClient = CreateProjectClient(); + var fcc = new FoundryChatClient(projectClient, "gpt-4o-mini"); + var agent = new ChatClientAgent(fcc, new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = "gpt-4o-mini", + Tools = new System.Collections.Generic.List { new UnsupportedTool() }, + }, + }); + var ex = await Assert.ThrowsAsync(() => agent.ToPromptAgentAsync()); + Assert.Contains(nameof(UnsupportedTool), ex.Message); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_HonorsCancellationAsync() + { + // Cancellation should bubble up from the AgentReference fetch path. Construct a + // FoundryAgent via AsAIAgent(AgentReference) and pass a pre-cancelled token. + var (foundryAgent, _) = CreateMode2_PromptAgentOnly("agent-name"); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => foundryAgent.ToPromptAgentAsync(cts.Token)); + } + + // ----- the Responses Agent mode (Mode 1) (RAPI) synthesis paths ----- + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_Mode1_RoundTripsModelInstructionsTemperatureTopPAsync() + { + var projectClient = CreateProjectClient(); + var fcc = new FoundryChatClient(projectClient, "gpt-4o-mini"); + var agent = new ChatClientAgent(fcc, new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = "gpt-4o-mini", + Instructions = "Be helpful.", + Temperature = 0.5f, + TopP = 0.9f, + }, + }); + + var def = await agent.ToPromptAgentAsync(); + var declarative = Assert.IsType(def); + Assert.Equal("gpt-4o-mini", declarative.Model); + Assert.Equal("Be helpful.", declarative.Instructions); + Assert.Equal(0.5f, declarative.Temperature); + Assert.Equal(0.9f, declarative.TopP); + Assert.Empty(declarative.Tools); + } + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_Mode1_NoTools_ReturnsDefinitionWithEmptyToolsAsync() + { + var projectClient = CreateProjectClient(); + var fcc = new FoundryChatClient(projectClient, "gpt-4o-mini"); + var agent = new ChatClientAgent(fcc, new ChatClientAgentOptions + { + ChatOptions = new ChatOptions { ModelId = "gpt-4o-mini" }, + }); + var def = await agent.ToPromptAgentAsync(); + var declarative = Assert.IsType(def); + Assert.Empty(declarative.Tools); + } + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_Mode1_AIFunctionTool_ConvertsToFunctionToolAsync() + { + var projectClient = CreateProjectClient(); + var fcc = new FoundryChatClient(projectClient, "gpt-4o-mini"); + var function = AIFunctionFactory.Create(() => "ok", "my_function", "A documented function."); + var agent = new ChatClientAgent(fcc, new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = "gpt-4o-mini", + Tools = new System.Collections.Generic.List { function }, + }, + }); + + var def = await agent.ToPromptAgentAsync(); + var declarative = Assert.IsType(def); + var fnTool = Assert.Single(declarative.Tools); + var ft = Assert.IsType(fnTool); + Assert.Equal("my_function", ft.FunctionName); + Assert.Equal("A documented function.", ft.FunctionDescription); + } + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_Mode1_FoundryAITool_UnwrapsUnderlyingResponseToolAsync() + { + var projectClient = CreateProjectClient(); + var fcc = new FoundryChatClient(projectClient, "gpt-4o-mini"); + var agent = new ChatClientAgent(fcc, new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = "gpt-4o-mini", + Tools = new System.Collections.Generic.List { FoundryAITool.CreateWebSearchTool() }, + }, + }); + + var def = await agent.ToPromptAgentAsync(); + var declarative = Assert.IsType(def); + var tool = Assert.Single(declarative.Tools); + // The unwrapped instance must be the concrete WebSearchTool from the OpenAI SDK. + Assert.IsType(tool); + } + + [Fact] + public async Task ToPromptAgentAsync_ChatClientAgent_Mode1_MultipleToolsMixed_ConvertsAllInOrderAsync() + { + var projectClient = CreateProjectClient(); + var fcc = new FoundryChatClient(projectClient, "gpt-4o-mini"); + var function = AIFunctionFactory.Create(() => "ok", "fn", ""); + var agent = new ChatClientAgent(fcc, new ChatClientAgentOptions + { + ChatOptions = new ChatOptions + { + ModelId = "gpt-4o-mini", + Tools = new System.Collections.Generic.List { function, FoundryAITool.CreateWebSearchTool() }, + }, + }); + + var def = await agent.ToPromptAgentAsync(); + var declarative = Assert.IsType(def); + Assert.Equal(2, declarative.Tools.Count); + Assert.IsType(declarative.Tools[0]); + Assert.IsType(declarative.Tools[1]); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_Mode1_ResultIsDeclarativeAgentDefinitionAsync() + { + // FoundryAgent constructed via the projectEndpoint+model+instructions ctor (Responses Agent mode, the Responses Agent mode (Mode 1)). + var foundryAgent = new FoundryAgent( + projectEndpoint: new Uri("https://test.openai.azure.com/"), + credential: new FakeAuthenticationTokenProvider(), + model: "gpt-4o-mini", + instructions: "You are helpful."); + + var def = await foundryAgent.ToPromptAgentAsync(); + var declarative = Assert.IsType(def); + Assert.Equal("gpt-4o-mini", declarative.Model); + Assert.Equal("You are helpful.", declarative.Instructions); + } + + // ----- the Prompt Agent mode (Mode 2) paths ----- + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_Mode2_AgentVersion_ReturnsCachedDefinitionAsync() + { + // Construct via ProjectsAgentVersion → the Definition reference must come back unchanged. + var version = ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentVersionResponseJson()))!; + var projectClient = CreateProjectClient(); + var foundryAgent = projectClient.AsAIAgent(version); + + var def = await foundryAgent.ToPromptAgentAsync(); + Assert.Same(version.Definition, def); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_Mode2_AgentRecord_ReturnsLatestVersionDefinitionAsync() + { + var record = ModelReaderWriter.Read(BinaryData.FromString(TestDataUtil.GetAgentResponseJson()))!; + var projectClient = CreateProjectClient(); + var foundryAgent = projectClient.AsAIAgent(record); + + var def = await foundryAgent.ToPromptAgentAsync(); + Assert.Same(record.GetLatestVersion().Definition, def); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_Mode2_PromptAgentOnly_FetchesLatestVersionAsync() + { + // The handler returns a known agent JSON. The converter must hit GET /agents/{name} + // and return that record's latest version definition. + var fetched = false; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Get && req.RequestUri!.AbsolutePath.Contains("/agents/agent-name")) + { + fetched = true; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(TestDataUtil.GetAgentResponseJson(agentName: "agent-name"), Encoding.UTF8, "application/json"), + }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var foundryAgent = projectClient.AsAIAgent(new AgentReference("agent-name")); + + var def = await foundryAgent.ToPromptAgentAsync(); + Assert.True(fetched); + Assert.NotNull(def); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_Mode2_PromptAgentOnly_PinnedVersion_FetchesPinnedVersionAsync() + { + // Q-C regression: when AgentReference.Version is set, the converter must call + // GET /agents/{name}/versions/{version} and return that pinned version's definition, + // NOT GET /agents/{name} -> GetLatestVersion() which would silently substitute the + // server's latest. We probe both paths from the same handler and assert exactly one was hit. + var fetchedLatest = false; + var fetchedPinned = false; + using var handler = new HttpHandlerAssert(req => + { + // Pinned-version path: …/agents/{name}/versions/{version} + if (req.Method == HttpMethod.Get && req.RequestUri!.AbsolutePath.Contains("/agents/agent-name/versions/2", StringComparison.Ordinal)) + { + fetchedPinned = true; + var pinnedDef = new DeclarativeAgentDefinition("gpt-pinned") { Instructions = "Pinned-version instructions." }; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(TestDataUtil.GetAgentVersionResponseJson(agentName: "agent-name", agentDefinition: pinnedDef), Encoding.UTF8, "application/json"), + }; + } + // Latest-version path: …/agents/{name} + if (req.Method == HttpMethod.Get && req.RequestUri!.AbsolutePath.EndsWith("/agents/agent-name", StringComparison.Ordinal)) + { + fetchedLatest = true; + var latestDef = new DeclarativeAgentDefinition("gpt-latest") { Instructions = "Latest-version instructions." }; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(TestDataUtil.GetAgentResponseJson(agentName: "agent-name", agentDefinition: latestDef), Encoding.UTF8, "application/json"), + }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var foundryAgent = projectClient.AsAIAgent(new AgentReference("agent-name", "2")); + + var def = await foundryAgent.ToPromptAgentAsync(); + + Assert.True(fetchedPinned, "Pinned-version endpoint (.../agents/agent-name/versions/2) must be called when AgentReference.Version is set."); + Assert.False(fetchedLatest, "Latest-version endpoint (.../agents/agent-name) must NOT be called when AgentReference.Version is set."); + var declarative = Assert.IsType(def); + Assert.Equal("gpt-pinned", declarative.Model); + Assert.Equal("Pinned-version instructions.", declarative.Instructions); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_Mode2_PromptAgentOnly_UnpinnedVersionKeyword_FetchesLatestAsync() + { + // Q-C boundary: AgentReference.Version == "latest" must fall back to the GET /agents/{name} + // path (the latest-version path), NOT GET /agents/{name}/versions/latest. + var fetchedLatest = false; + using var handler = new HttpHandlerAssert(req => + { + if (req.Method == HttpMethod.Get && req.RequestUri!.AbsolutePath.EndsWith("/agents/agent-name", StringComparison.Ordinal)) + { + fetchedLatest = true; + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(TestDataUtil.GetAgentResponseJson(agentName: "agent-name"), Encoding.UTF8, "application/json"), + }; + } + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("{}", Encoding.UTF8, "application/json") }; + }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var foundryAgent = projectClient.AsAIAgent(new AgentReference("agent-name", "latest")); + + var def = await foundryAgent.ToPromptAgentAsync(); + + Assert.True(fetchedLatest); + Assert.NotNull(def); + } + + [Fact] + public async Task ToPromptAgentAsync_FoundryAgent_Mode2_PromptAgentOnly_ServerReturnsError_PropagatesExceptionAsync() + { + using var handler = new HttpHandlerAssert(req => + new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("{\"error\":{\"code\":\"NotFound\"}}", Encoding.UTF8, "application/json") }); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + var projectClient = new AIProjectClient( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(httpClient) }); + var foundryAgent = projectClient.AsAIAgent(new AgentReference("missing-agent")); + + await Assert.ThrowsAnyAsync(() => foundryAgent.ToPromptAgentAsync()); + } + + // ----- Python-parity guard: both extensions produce equivalent definitions ----- + + [Fact] + public async Task BothExtensions_ProduceEquivalentDefinitions_ForEquivalentInputsAsync() + { + // Build two agents that are semantically equivalent: one as a plain ChatClientAgent + // via AsAIAgent(model, instructions), and one as a FoundryAgent via the projectEndpoint + // ctor. Both flow through the same converter; assert key fields match. + var projectClient = CreateProjectClient(); + ChatClientAgent ccaAgent = projectClient.AsAIAgent("gpt-4o-mini", "Be helpful."); + var foundryAgent = new FoundryAgent( + projectEndpoint: new Uri("https://test.openai.azure.com/"), + credential: new FakeAuthenticationTokenProvider(), + model: "gpt-4o-mini", + instructions: "Be helpful."); + + var ccaDef = await ccaAgent.ToPromptAgentAsync(); + var faDef = await foundryAgent.ToPromptAgentAsync(); + + var a = Assert.IsType(ccaDef); + var b = Assert.IsType(faDef); + Assert.Equal(a.Model, b.Model); + Assert.Equal(a.Instructions, b.Instructions); + Assert.Equal(a.Tools.Count, b.Tools.Count); + } + + // ----- Helpers ----- + + private static AIProjectClient CreateProjectClient() + => new( + new Uri("https://test.openai.azure.com/"), + new FakeAuthenticationTokenProvider(), + new AIProjectClientOptions { Transport = new HttpClientPipelineTransport(new HttpClient()) }); + + private static (FoundryAgent FoundryAgent, AIProjectClient ProjectClient) CreateMode2_PromptAgentOnly(string agentName) + { + var projectClient = CreateProjectClient(); + var foundryAgent = projectClient.AsAIAgent(new AgentReference(agentName)); + return (foundryAgent, projectClient); + } + + private sealed class NoOpChatClient : IChatClient + { + public Task GetResponseAsync(System.Collections.Generic.IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => Task.FromResult(new ChatResponse()); + + public System.Collections.Generic.IAsyncEnumerable GetStreamingResponseAsync(System.Collections.Generic.IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => EmptyAsyncEnumerableAsync(); + + private static async System.Collections.Generic.IAsyncEnumerable EmptyAsyncEnumerableAsync() + { + await Task.CompletedTask.ConfigureAwait(false); + yield break; + } + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() { } + } + + private sealed class UnsupportedTool : AITool + { + public override string Name => "unsupported"; + } +} +#pragma warning restore CS0618 diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/MeaiAutoUserAgentVerificationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/MeaiAutoUserAgentVerificationTests.cs new file mode 100644 index 0000000000..050dd43a22 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/MeaiAutoUserAgentVerificationTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using OpenAI; +using OpenAI.Responses; + +#pragma warning disable OPENAI001 + +namespace Microsoft.Agents.AI.Foundry.UnitTests; + +/// +/// One-shot verification (kept in tree to detect regressions) that MEAI 10.5.1 stamps its own +/// MEAI/{version} User-Agent segment automatically when an +/// is wrapped via AsIChatClient(). If this test starts failing, the FoundryChatClient +/// implementation must re-register the MEAI policy explicitly via OpenAIRequestPolicies because +/// the local Foundry copy was deleted under the assumption that MEAI provides it built-in. +/// +public sealed class MeaiAutoUserAgentVerificationTests +{ + [Fact] + public async Task MeaiOpenAIResponsesClient_StampsMeaiSegmentAutomatically_WithoutLocalPolicyAsync() + { + // Arrange: bare OpenAI ResponseClient over a fake HTTP transport, wrapped via MEAI's + // AsIChatClient() with no custom OpenAIRequestPolicies registration. If MEAI auto-stamps + // its own MEAI/{version} segment, it will appear here. + using var handler = new RecordingHandler(); +#pragma warning disable CA5399 + using var httpClient = new HttpClient(handler); +#pragma warning restore CA5399 + + var options = new OpenAIClientOptions + { + Transport = new HttpClientPipelineTransport(httpClient), + Endpoint = new Uri("https://example.test/v1"), + }; + + var responseClient = new ResponsesClient(new ApiKeyCredential("test-key"), options); + var chatClient = responseClient.AsIChatClient("gpt-4o-mini"); + + // Act: send a request through MEAI's chat client. The fake transport will throw on + // response parsing, but we only care about the outbound headers, which are captured + // before the response is parsed. + try + { + await chatClient.GetResponseAsync("hi", cancellationToken: CancellationToken.None); + } + catch + { + // Expected: the fake response body is not parseable as a Responses API payload. + } + + // Assert: at least one outbound request reached the transport, and its User-Agent + // contains either "MEAI/" (auto-stamped by MEAI) or no MEAI segment (verification + // signal — see test summary). + Assert.True(handler.Count > 0, "Expected at least one outbound request from MEAI wrapper."); + Assert.NotNull(handler.LastUserAgent); + // INTENT: assert that MEAI auto-stamps. If the assertion fails, see the FoundryChatClient + // implementation note about needing to register the MEAI policy explicitly. + Assert.Contains("MEAI/", handler.LastUserAgent); + } + + private sealed class RecordingHandler : HttpClientHandler + { + public int Count { get; private set; } + public string? LastUserAgent { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.Count++; + this.LastUserAgent = request.Headers.TryGetValues("User-Agent", out var values) + ? string.Join(",", values) + : null; + + var resp = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}", Encoding.UTF8, "application/json"), + RequestMessage = request, + }; + return Task.FromResult(resp); + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs deleted file mode 100644 index df5dd8ebae..0000000000 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.UnitTests/RequestOptionsExtensionsTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Agents.AI.Foundry.UnitTests; - -/// -/// Verifies the per-call MeaiUserAgentPolicy exposed via -/// . The policy is reachable through the -/// public constructors (which add it to the internally-built -/// 's pipeline), so its behavior is part of the -/// public API surface. -/// -public sealed class RequestOptionsExtensionsTests -{ - [Fact] - public async Task MeaiUserAgentPolicy_AddsMeaiSegment_ToOutgoingRequestAsync() - { - // Arrange - using var handler = new RecordingHandler(); -#pragma warning disable CA5399 - using var httpClient = new HttpClient(handler); -#pragma warning restore CA5399 - var pipeline = ClientPipeline.Create( - new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, - perCallPolicies: [RequestOptionsExtensions.UserAgentPolicy], - perTryPolicies: default, - beforeTransportPolicies: default); - - // Act - var message = pipeline.CreateMessage(); - message.Request.Method = "POST"; - message.Request.Uri = new System.Uri("https://example.test/anything"); - await pipeline.SendAsync(message); - - // Assert - Assert.Equal(1, handler.Count); - Assert.NotNull(handler.LastUserAgent); - Assert.Contains("MEAI/", handler.LastUserAgent); - } - - [Fact] - public async Task MeaiUserAgentPolicy_DoesNotAddFoundryHostingSegmentAsync() - { - // Arrange - using var handler = new RecordingHandler(); -#pragma warning disable CA5399 - using var httpClient = new HttpClient(handler); -#pragma warning restore CA5399 - var pipeline = ClientPipeline.Create( - new ClientPipelineOptions { Transport = new HttpClientPipelineTransport(httpClient) }, - perCallPolicies: [RequestOptionsExtensions.UserAgentPolicy], - perTryPolicies: default, - beforeTransportPolicies: default); - - // Act - var message = pipeline.CreateMessage(); - message.Request.Method = "POST"; - message.Request.Uri = new System.Uri("https://example.test/anything"); - await pipeline.SendAsync(message); - - // Assert: the policy is MEAI-only; the foundry-hosting supplement is added elsewhere - // (by the polyfill UserAgentResponsesClient → HostedAgentUserAgentPolicy). - Assert.NotNull(handler.LastUserAgent); - Assert.DoesNotContain("foundry-hosting/agent-framework-dotnet", handler.LastUserAgent); - } - - [Fact] - public void UserAgentPolicy_ExposesSingletonInstance() - { - // Two reads of the static property must return the same instance — the policy is stateless and shared. - var first = RequestOptionsExtensions.UserAgentPolicy; - var second = RequestOptionsExtensions.UserAgentPolicy; - Assert.Same(first, second); - } - - [Fact] - public void MeaiUserAgentPolicy_ValueIncludesAFFoundryAssemblyVersion_ReflectionGuard() - { - // The policy emits "MEAI/{Microsoft.Agents.AI.Foundry assembly InformationalVersion}". - // If the assembly metadata stops being readable, the policy falls back to "MEAI" without a version, - // which is a measurable telemetry regression. - var attr = typeof(RequestOptionsExtensions).Assembly - .GetCustomAttribute(); - Assert.NotNull(attr); - Assert.False(string.IsNullOrEmpty(attr!.InformationalVersion)); - } - - private sealed class RecordingHandler : HttpClientHandler - { - public int Count { get; private set; } - public string? LastUserAgent { get; private set; } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - this.Count++; - this.LastUserAgent = request.Headers.TryGetValues("User-Agent", out var values) - ? string.Join(",", values) - : null; - - var resp = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{}", Encoding.UTF8, "application/json"), - RequestMessage = request, - }; - return Task.FromResult(resp); - } - } -} From 405010794207e9db952da0e4331f296be2e1595b Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 21 May 2026 11:57:06 +0100 Subject: [PATCH 19/22] .NET: Add background agents support to HarnessAgent (#5977) * Add background agents support to HarnessAgent * Add unit tests * Address PR comments --- .../Program.cs | 5 +- .../HarnessAgent.cs | 10 ++ .../HarnessAgentOptions.cs | 22 +++ .../HarnessAgentOptionsTests.cs | 8 + .../HarnessAgentTests.cs | 150 ++++++++++++++++++ 5 files changed, 191 insertions(+), 4 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs index e749adb5af..679bdfe870 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs @@ -102,10 +102,7 @@ AIAgent parentAgent = DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory DisableToolApproval = true, // If enabled, this allows don't-ask-again approval functionality. DisableWebSearch = true, - AIContextProviders = - [ - new BackgroundAgentsProvider([webSearchAgent]), - ], + BackgroundAgents = [webSearchAgent], ChatOptions = new ChatOptions { Instructions = parentInstructions, diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs index 9633a05ed8..e8ce93a787 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using Microsoft.Agents.AI.Compaction; using Microsoft.Extensions.AI; using Microsoft.Shared.DiagnosticIds; @@ -249,6 +250,15 @@ public sealed class HarnessAgent : DelegatingAIAgent providers.Add(skillsProvider); } + if (options?.BackgroundAgents is IEnumerable backgroundAgents) + { + var materializedAgents = backgroundAgents.ToList(); + if (materializedAgents.Count > 0) + { + providers.Add(new BackgroundAgentsProvider(materializedAgents, options.BackgroundAgentsProviderOptions)); + } + } + if (options?.AIContextProviders is IEnumerable userProviders) { providers.AddRange(userProviders); diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs index cdcbb36605..5291647882 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs @@ -218,4 +218,26 @@ public sealed class HarnessAgentOptions /// This property is ignored when is . /// public string? OpenTelemetrySourceName { get; set; } + + /// + /// Gets or sets the collection of background agents available for delegation via . + /// + /// + /// When non-null and non-empty, a is automatically included in the + /// agent's context providers, enabling the agent to start, monitor, and retrieve results from background tasks. + /// When or empty, no is configured. + /// Each agent in the collection must have a non-empty and names must be unique + /// (case-insensitive). If these requirements are not met, will throw + /// an during construction. + /// + public IEnumerable? BackgroundAgents { get; set; } + + /// + /// Gets or sets optional configuration for the . + /// + /// + /// Use this to customize instructions or agent list formatting for the background agents feature. + /// This property is ignored when is or empty. + /// + public BackgroundAgentsProviderOptions? BackgroundAgentsProviderOptions { get; set; } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs index c78702fa6f..d880300cd0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs @@ -37,6 +37,8 @@ public class HarnessAgentOptionsTests Assert.Null(options.FileAccessStore); Assert.Null(options.AgentModeProviderOptions); Assert.Null(options.AgentSkillsSource); + Assert.Null(options.BackgroundAgents); + Assert.Null(options.BackgroundAgentsProviderOptions); } /// @@ -52,6 +54,8 @@ public class HarnessAgentOptionsTests var fileAccessStore = new Mock().Object; var agentModeOptions = new AgentModeProviderOptions(); var skillsSource = new Mock().Object; + var backgroundAgents = new AIAgent[] { new Mock().Object }; + var backgroundAgentsOptions = new BackgroundAgentsProviderOptions(); // Act var options = new HarnessAgentOptions @@ -77,6 +81,8 @@ public class HarnessAgentOptionsTests AgentSkillsSource = skillsSource, DisableOpenTelemetry = true, OpenTelemetrySourceName = "custom-source", + BackgroundAgents = backgroundAgents, + BackgroundAgentsProviderOptions = backgroundAgentsOptions, }; // Assert @@ -103,5 +109,7 @@ public class HarnessAgentOptionsTests Assert.Same(skillsSource, options.AgentSkillsSource); Assert.True(options.DisableOpenTelemetry); Assert.Equal("custom-source", options.OpenTelemetrySourceName); + Assert.Same(backgroundAgents, options.BackgroundAgents); + Assert.Same(backgroundAgentsOptions, options.BackgroundAgentsProviderOptions); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index 728025c563..8eb61da488 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -1197,4 +1197,154 @@ public class HarnessAgentTests } #endregion + + #region Feature: BackgroundAgentsProvider + + /// + /// Verify that BackgroundAgentsProvider is included when BackgroundAgents are specified. + /// + [Fact] + public void BackgroundAgentsProvider_IncludedWhenAgentsSpecified() + { + // Arrange + var chatClient = new Mock().Object; + var bgAgentMock = new Mock(); + bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent"); + var options = CreateAllDisabledOptions(); + options.BackgroundAgents = [bgAgentMock.Object]; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options); + var innerAgent = agent.GetService(); + + // Assert + Assert.NotNull(innerAgent?.AIContextProviders); + Assert.Contains(innerAgent!.AIContextProviders!, p => p is BackgroundAgentsProvider); + } + + /// + /// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is null. + /// + [Fact] + public void BackgroundAgentsProvider_ExcludedWhenAgentsNull() + { + // Arrange + var chatClient = new Mock().Object; + var options = CreateAllDisabledOptions(); + options.BackgroundAgents = null; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options); + var innerAgent = agent.GetService(); + + // Assert + Assert.NotNull(innerAgent); + if (innerAgent!.AIContextProviders != null) + { + Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider); + } + } + + /// + /// Verify that BackgroundAgentsProvider is not included when BackgroundAgents is an empty collection. + /// + [Fact] + public void BackgroundAgentsProvider_ExcludedWhenAgentsEmpty() + { + // Arrange + var chatClient = new Mock().Object; + var options = CreateAllDisabledOptions(); + options.BackgroundAgents = Array.Empty(); + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options); + var innerAgent = agent.GetService(); + + // Assert + Assert.NotNull(innerAgent); + if (innerAgent!.AIContextProviders != null) + { + Assert.DoesNotContain(innerAgent.AIContextProviders, p => p is BackgroundAgentsProvider); + } + } + + /// + /// Verify that BackgroundAgentsProviderOptions is passed through when specified. + /// + [Fact] + public async Task BackgroundAgentsProvider_UsesProvidedOptionsAsync() + { + // Arrange + var chatClient = new Mock().Object; + var bgAgentMock = new Mock(); + bgAgentMock.Setup(a => a.Name).Returns("TestBackgroundAgent"); + bgAgentMock.Setup(a => a.Description).Returns("A test background agent"); + var providerOptions = new BackgroundAgentsProviderOptions + { + Instructions = "Custom instructions with {background_agents} list.", + }; + var options = CreateAllDisabledOptions(); + options.BackgroundAgents = [bgAgentMock.Object]; + options.BackgroundAgentsProviderOptions = providerOptions; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options); + var innerAgent = agent.GetService(); + var bgProvider = innerAgent!.AIContextProviders!.OfType().Single(); + +#pragma warning disable MAAI001 + var invokingContext = new AIContextProvider.InvokingContext( + new Mock().Object, + new Mock().Object, + new AIContext()); +#pragma warning restore MAAI001 + + AIContext result = await bgProvider.InvokingAsync(invokingContext); + + // Assert — custom instructions template is used and agent info is included + Assert.NotNull(result.Instructions); + Assert.Contains("Custom instructions with", result.Instructions); + Assert.Contains("TestBackgroundAgent", result.Instructions); + } + + /// + /// Verify that multiple background agents are all passed to the provider. + /// + [Fact] + public async Task BackgroundAgentsProvider_IncludesMultipleAgentsAsync() + { + // Arrange + var chatClient = new Mock().Object; + var agent1Mock = new Mock(); + agent1Mock.Setup(a => a.Name).Returns("Agent1"); + agent1Mock.Setup(a => a.Description).Returns("First agent"); + var agent2Mock = new Mock(); + agent2Mock.Setup(a => a.Name).Returns("Agent2"); + agent2Mock.Setup(a => a.Description).Returns("Second agent"); + var options = CreateAllDisabledOptions(); + options.BackgroundAgents = [agent1Mock.Object, agent2Mock.Object]; + + // Act + var agent = new HarnessAgent(chatClient, TestMaxContextWindowTokens, TestMaxOutputTokens, options); + var innerAgent = agent.GetService(); + var bgProvider = innerAgent!.AIContextProviders!.OfType().Single(); + +#pragma warning disable MAAI001 + var invokingContext = new AIContextProvider.InvokingContext( + new Mock().Object, + new Mock().Object, + new AIContext()); +#pragma warning restore MAAI001 + + AIContext result = await bgProvider.InvokingAsync(invokingContext); + + // Assert — both agents appear in the provider's instructions + Assert.NotNull(result.Instructions); + Assert.Contains("Agent1", result.Instructions); + Assert.Contains("First agent", result.Instructions); + Assert.Contains("Agent2", result.Instructions); + Assert.Contains("Second agent", result.Instructions); + } + + #endregion } From 46326b6b93fb38817a9d1f25369e55b5aa970e78 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 21 May 2026 14:54:47 +0100 Subject: [PATCH 20/22] .NET: Add additional openai specific error observers and move them to openai project (#6004) * Add additional openai specific error observers and move them to openai project * Address PR comments --- dotnet/agent-framework-dotnet.slnx | 1 + .../HarnessAgentRunner.cs | 5 ++ .../Observers/ConsoleObserver.cs | 11 ++++ .../Harness_Shared_Console_OpenAI.csproj | 19 ++++++ .../OpenAIResponsesErrorObserver.cs | 61 +++++++++++++++++++ ...OpenAIResponsesWebSearchDisplayObserver.cs | 5 +- .../Harness_Step01_Research.csproj | 1 + .../Harness_Step01_Research/Program.cs | 2 + ...tep02_Research_WithBackgroundAgents.csproj | 1 + .../Program.cs | 7 ++- 10 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/Harness_Shared_Console_OpenAI.csproj create mode 100644 dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs rename dotnet/samples/02-agents/Harness/{Harness_Step01_Research => Harness_Shared_Console_OpenAI}/OpenAIResponsesWebSearchDisplayObserver.cs (98%) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index d694c10cbc..76a1f20a74 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -121,6 +121,7 @@ + diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs index 7d4dea3156..0b1de1bf86 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/HarnessAgentRunner.cs @@ -192,6 +192,11 @@ public sealed class HarnessAgentRunner : IDisposable } } + foreach (var observer in this._observers) + { + await observer.OnResponseUpdateAsync(this._ux, update, this._agent, this._session).ConfigureAwait(false); + } + if (!string.IsNullOrEmpty(update.Text)) { foreach (var observer in this._observers) diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs index 0a0307f661..f5a04d9719 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console/Observers/ConsoleObserver.cs @@ -24,6 +24,17 @@ public abstract class ConsoleObserver { } + /// + /// Called for each in the response stream, regardless of + /// whether it contains content. Override to inspect update-level metadata such as + /// for provider-specific events. + /// + /// The UX state driver, used for rendering output. + /// The streaming response update. + /// The agent being interacted with. + /// The current agent session. + public virtual Task OnResponseUpdateAsync(IUXStateDriver ux, AgentResponseUpdate update, AIAgent agent, AgentSession session) => Task.CompletedTask; + /// /// Called for each item in the response stream. /// diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/Harness_Shared_Console_OpenAI.csproj b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/Harness_Shared_Console_OpenAI.csproj new file mode 100644 index 0000000000..cbf8c7dda3 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/Harness_Shared_Console_OpenAI.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + + enable + enable + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs new file mode 100644 index 0000000000..9db00c9a65 --- /dev/null +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesErrorObserver.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage. + +using Harness.Shared.Console.Observers; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using OpenAI.Responses; + +namespace Harness.Shared.Console.OpenAI; + +/// +/// Detects and displays error/incomplete status from OpenAI Responses API streaming updates. +/// Handles and +/// which are not surfaced as by the chat client. +/// +/// +/// Note: is already handled by the SDK — it produces +/// an which is displayed by . +/// This observer covers the cases where the SDK does not produce . +/// +public sealed class OpenAIResponsesErrorObserver : ConsoleObserver +{ + /// + public override async Task OnResponseUpdateAsync(IUXStateDriver ux, AgentResponseUpdate update, AIAgent agent, AgentSession session) + { + // AgentResponseUpdate.RawRepresentation is the ChatResponseUpdate, + // whose RawRepresentation is the underlying StreamingResponseUpdate. + object? rawUpdate = (update.RawRepresentation as ChatResponseUpdate)?.RawRepresentation + ?? update.RawRepresentation; + + switch (rawUpdate) + { + case StreamingResponseFailedUpdate failedUpdate: + // Only display if the response has error details populated. + // When error is null, a follow-up StreamingResponseErrorUpdate typically + // carries the real error — the SDK surfaces that as ErrorContent, + // which is displayed by ErrorDisplayObserver. + if (failedUpdate.Response?.Error is { } error) + { + string errorMessage = error.Message ?? "Unknown error"; + string? errorCode = error.Code.ToString(); + string errorText = $"❌ Response failed: {errorMessage}"; + if (!string.IsNullOrEmpty(errorCode)) + { + errorText += $" (code: {errorCode})"; + } + + await ux.WriteInfoLineAsync(errorText, ConsoleColor.Red); + } + + break; + + case StreamingResponseIncompleteUpdate incompleteUpdate: + string? reason = incompleteUpdate.Response?.IncompleteStatusDetails?.Reason?.ToString(); + string incompleteText = $"⚠️ Response incomplete: {reason ?? "unknown reason"}"; + await ux.WriteInfoLineAsync(incompleteText, ConsoleColor.Yellow); + break; + } + } +} diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/OpenAIResponsesWebSearchDisplayObserver.cs b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesWebSearchDisplayObserver.cs similarity index 98% rename from dotnet/samples/02-agents/Harness/Harness_Step01_Research/OpenAIResponsesWebSearchDisplayObserver.cs rename to dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesWebSearchDisplayObserver.cs index f680ea6ff3..0a81685493 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/OpenAIResponsesWebSearchDisplayObserver.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Shared_Console_OpenAI/OpenAIResponsesWebSearchDisplayObserver.cs @@ -3,19 +3,18 @@ #pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage. using System.Text; -using Harness.Shared.Console; using Harness.Shared.Console.Observers; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI.Responses; -namespace SampleApp; +namespace Harness.Shared.Console.OpenAI; /// /// Displays web search activity in the scroll area. Shows search queries, /// page opens, and find-in-page actions as they stream in from the API. /// -internal sealed class OpenAIResponsesWebSearchDisplayObserver : ConsoleObserver +public sealed class OpenAIResponsesWebSearchDisplayObserver : ConsoleObserver { private const int MaxQueryDisplayLength = 120; diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj index 5f0e9ef024..af3e391b3d 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj +++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Harness_Step01_Research.csproj @@ -16,6 +16,7 @@ + diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs index dc5d26978c..4c4010f0c0 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs @@ -19,6 +19,7 @@ using System.ClientModel.Primitives; using Azure.AI.Projects; using Azure.Identity; using Harness.Shared.Console; +using Harness.Shared.Console.OpenAI; using Harness.Shared.Console.ToolFormatters; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; @@ -107,6 +108,7 @@ await HarnessConsole.RunAgentAsync( { Observers = [ new OpenAIResponsesWebSearchDisplayObserver(), + new OpenAIResponsesErrorObserver(), .. HarnessConsoleOptions.BuildObserversWithPlanning( agent, planModeName: "plan", diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj index 5f0e9ef024..af3e391b3d 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj +++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Harness_Step02_Research_WithBackgroundAgents.csproj @@ -16,6 +16,7 @@ + diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs index 679bdfe870..e8e10a3620 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs @@ -16,6 +16,7 @@ using System.ClientModel.Primitives; using Azure.AI.Projects; using Azure.Identity; using Harness.Shared.Console; +using Harness.Shared.Console.OpenAI; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; @@ -113,4 +114,8 @@ AIAgent parentAgent = // Run the interactive console session. await HarnessConsole.RunAgentAsync( parentAgent, - userPrompt: "Enter a list of stock tickers (e.g., BAC, MSFT, BA):"); + userPrompt: "Enter a list of stock tickers (e.g., BAC, MSFT, BA):", + options: new HarnessConsoleOptions + { + Observers = [new OpenAIResponsesErrorObserver(), .. HarnessConsoleOptions.BuildDefaultObservers()], + }); From 289cafcf36c816dd05b5899747242d8853c70149 Mon Sep 17 00:00:00 2001 From: Giles Odigwe <79032838+giles17@users.noreply.github.com> Date: Thu, 21 May 2026 08:04:56 -0700 Subject: [PATCH 21/22] Python: feat(a2a): use non-streaming transport and return_immediately for background ops (#5963) * feat(a2a): use non-streaming transport and return_immediately for background ops When stream=False, use a client configured with streaming=False so the SDK sends a single HTTP POST to message/send instead of opening an SSE connection via message/stream. This matches the A2A protocol's design: non-streaming calls use direct request/response, streaming calls use Server-Sent Events. Also sets return_immediately=background on SendMessageConfiguration so the server respects the caller's intent for background operations. Changes: - Create separate streaming and non-streaming internal clients (sharing the same httpx connection pool) to match protocol transport semantics - Select non-streaming client for run(stream=False) calls - Add SendMessageConfiguration with return_immediately=background - Fallback to streaming client when non-streaming unavailable (e.g. user provides their own client via constructor) - Add tests for client selection and return_immediately behavior Resolves microsoft/agent-framework#5936 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review feedback - Initialize last_request in MockA2AClient.__init__ for explicit state - Use 'is not None' instead of truthiness for _non_streaming_client check - Assert return_immediately propagates through non-streaming client path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: only set configuration when background=True Only attach SendMessageConfiguration to the request when background=True, keeping requests minimal and preserving server-side defaults for normal (foreground) operations. This follows the framework pattern of only setting optional fields when they have meaningful values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: only set return_immediately for non-streaming background ops Per the A2A spec, return_immediately only applies to message/send (non-streaming). It has no effect on streaming operations. Only set the configuration field when both background=True and stream=False. Adds test verifying streaming+background does not set return_immediately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../a2a/agent_framework_a2a/_agent.py | 43 +++++++-- python/packages/a2a/tests/test_a2a_agent.py | 92 +++++++++++++++++++ 2 files changed, 128 insertions(+), 7 deletions(-) diff --git a/python/packages/a2a/agent_framework_a2a/_agent.py b/python/packages/a2a/agent_framework_a2a/_agent.py index bc175ccc48..bb2baf1bb5 100644 --- a/python/packages/a2a/agent_framework_a2a/_agent.py +++ b/python/packages/a2a/agent_framework_a2a/_agent.py @@ -129,6 +129,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): self._timeout_config = self._create_timeout_config(timeout) if client is not None: self.client = client + self._non_streaming_client: Client | None = None self._close_http_client = True return if agent_card is None: @@ -144,17 +145,30 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): self._http_client = http_client # Store for cleanup self._close_http_client = True - # Create A2A client using factory - config = ClientConfig( + interceptors = [auth_interceptor] if auth_interceptor is not None else None + + # Create streaming client (SSE transport for stream=True) + streaming_config = ClientConfig( httpx_client=http_client, + streaming=True, supported_protocol_bindings=["JSONRPC"], ) - factory = ClientFactory(config) - interceptors = [auth_interceptor] if auth_interceptor is not None else None + # Create non-streaming client (single request/response for stream=False) + non_streaming_config = ClientConfig( + httpx_client=http_client, + streaming=False, + supported_protocol_bindings=["JSONRPC"], + ) + streaming_factory = ClientFactory(streaming_config) + non_streaming_factory = ClientFactory(non_streaming_config) # Attempt transport negotiation with the provided agent card try: - self.client = factory.create(agent_card, interceptors=interceptors) # type: ignore + self.client = streaming_factory.create(agent_card, interceptors=interceptors) # type: ignore + self._non_streaming_client = non_streaming_factory.create( + agent_card, + interceptors=interceptors, # type: ignore + ) except Exception as transport_error: # Transport negotiation failed - fall back to minimal agent card with JSONRPC fallback_url = agent_card.supported_interfaces[0].url if agent_card.supported_interfaces else url @@ -166,7 +180,11 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): ) from transport_error fallback_card = minimal_agent_card(fallback_url, ["JSONRPC"]) try: - self.client = factory.create(fallback_card, interceptors=interceptors) # type: ignore + self.client = streaming_factory.create(fallback_card, interceptors=interceptors) # type: ignore + self._non_streaming_client = non_streaming_factory.create( + fallback_card, + interceptors=interceptors, # type: ignore + ) except Exception as fallback_error: raise RuntimeError( f"A2A transport negotiation failed. " @@ -282,6 +300,13 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): del function_invocation_kwargs, client_kwargs, kwargs normalized_messages = normalize_messages(messages) + # Use non-streaming transport for non-streaming calls when available. + # This sends a single HTTP request/response instead of opening an SSE + # connection, matching the protocol's intent for synchronous operations. + active_client = ( + self._non_streaming_client if (not stream and self._non_streaming_client is not None) else self.client + ) + if continuation_token is not None: a2a_stream: AsyncIterable[A2AStreamItem] = self.client.subscribe( SubscribeToTaskRequest(id=continuation_token["task_id"]) @@ -293,7 +318,11 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent): normalized_messages[-1], context_id=session.service_session_id if session else None, ) - a2a_stream = self.client.send_message(SendMessageRequest(message=a2a_message)) + request = SendMessageRequest(message=a2a_message) + if background and not stream: + # return_immediately only applies to non-streaming (message/send) + request.configuration.return_immediately = True + a2a_stream = active_client.send_message(request) provider_session = session if provider_session is None and self.context_providers: diff --git a/python/packages/a2a/tests/test_a2a_agent.py b/python/packages/a2a/tests/test_a2a_agent.py index 76294f30bf..f5474bc374 100644 --- a/python/packages/a2a/tests/test_a2a_agent.py +++ b/python/packages/a2a/tests/test_a2a_agent.py @@ -44,6 +44,7 @@ class MockA2AClient: self.subscribe_responses: list[StreamResponse] = [] self.get_task_response: Task | None = None self.last_message: Any = None + self.last_request: Any = None def add_message_response(self, message_id: str, text: str, role: str = "agent") -> None: """Add a mock Message response.""" @@ -91,6 +92,7 @@ class MockA2AClient: async def send_message(self, request: Any) -> AsyncIterator[StreamResponse]: """Mock send_message method that yields responses.""" + self.last_request = request self.last_message = getattr(request, "message", request) self.call_count += 1 @@ -745,6 +747,96 @@ async def test_working_task_no_token_without_background(a2a_agent: A2AAgent, moc assert response.continuation_token is None +async def test_background_sets_return_immediately_on_request( + a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient +) -> None: + """Test that background=True sets return_immediately=True on SendMessageRequest configuration.""" + mock_a2a_client.add_in_progress_task_response("task-bg", state=TaskState.TASK_STATE_WORKING) + + await a2a_agent.run("Background task", background=True) + + assert mock_a2a_client.last_request.configuration.return_immediately is True + + +async def test_foreground_does_not_set_return_immediately( + a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient +) -> None: + """Test that background=False (default) does not set configuration on SendMessageRequest.""" + mock_a2a_client.add_task_response("task-fg2", [{"id": "art-1", "content": "Done"}]) + + await a2a_agent.run("Foreground task") + + assert mock_a2a_client.last_request.HasField("configuration") is False + + +async def test_streaming_background_does_not_set_return_immediately( + a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient +) -> None: + """Test that background=True with stream=True does not set return_immediately. + + Per A2A spec, return_immediately only applies to non-streaming (message/send). + """ + mock_a2a_client.add_task_response("task-sb", [{"id": "art-1", "content": "Streaming bg"}]) + + updates: list[AgentResponseUpdate] = [] + async for update in a2a_agent.run("Stream background", stream=True, background=True): + updates.append(update) + + assert mock_a2a_client.last_request.HasField("configuration") is False + + +async def test_non_streaming_run_uses_non_streaming_client() -> None: + """Test that stream=False uses the non-streaming client when available.""" + streaming_client = MockA2AClient() + non_streaming_client = MockA2AClient() + non_streaming_client.add_task_response("task-ns", [{"id": "art-1", "content": "Non-streaming result"}]) + + agent = A2AAgent(name="Test Agent", id="test-ns", client=streaming_client, http_client=None) + agent._non_streaming_client = non_streaming_client # type: ignore[assignment] + + response = await agent.run("Hello") + + # Non-streaming client should have been called + assert non_streaming_client.call_count == 1 + assert streaming_client.call_count == 0 + assert response.messages[0].text == "Non-streaming result" + assert non_streaming_client.last_request.HasField("configuration") is False + + +async def test_streaming_run_uses_streaming_client() -> None: + """Test that stream=True always uses the streaming client.""" + streaming_client = MockA2AClient() + non_streaming_client = MockA2AClient() + streaming_client.add_task_response("task-s", [{"id": "art-1", "content": "Streaming result"}]) + + agent = A2AAgent(name="Test Agent", id="test-s", client=streaming_client, http_client=None) + agent._non_streaming_client = non_streaming_client # type: ignore[assignment] + + updates: list[AgentResponseUpdate] = [] + async for update in agent.run("Hello", stream=True): + updates.append(update) + + # Streaming client should have been called + assert streaming_client.call_count == 1 + assert non_streaming_client.call_count == 0 + assert updates[0].contents[0].text == "Streaming result" + + +async def test_non_streaming_client_fallback_when_not_available( + a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient +) -> None: + """Test that stream=False falls back to streaming client when non-streaming client is unavailable.""" + mock_a2a_client.add_task_response("task-fb", [{"id": "art-1", "content": "Fallback result"}]) + + # a2a_agent is created with client= param so _non_streaming_client is None + assert a2a_agent._non_streaming_client is None + + response = await a2a_agent.run("Hello") + + assert mock_a2a_client.call_count == 1 + assert response.messages[0].text == "Fallback result" + + async def test_completed_task_has_no_continuation_token(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None: """Test that a completed task does not set a continuation token.""" mock_a2a_client.add_task_response("task-done", [{"id": "art-1", "content": "Result"}]) From 46ed66cfd531fad3dd47d88f705f7424b1be4a29 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 22 May 2026 00:23:36 +0800 Subject: [PATCH 22/22] Python: include tool definitions for Foundry agent evals (#5974) --- .../agent_framework_foundry/_foundry_evals.py | 15 ++++++++++- .../foundry/tests/test_foundry_evals.py | 25 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_foundry_evals.py b/python/packages/foundry/agent_framework_foundry/_foundry_evals.py index 2f68816591..eef58b0a04 100644 --- a/python/packages/foundry/agent_framework_foundry/_foundry_evals.py +++ b/python/packages/foundry/agent_framework_foundry/_foundry_evals.py @@ -75,6 +75,15 @@ _TOOL_EVALUATORS: set[str] = { "builtin.tool_call_success", } +# Evaluators that accept tool_definitions in their data mapping when the +# evaluated items include tools. +_TOOL_DEFINITION_EVALUATORS: set[str] = _TOOL_EVALUATORS | { + "builtin.intent_resolution", + "builtin.task_adherence", + "builtin.task_completion", + "builtin.task_navigation_efficiency", +} + # Evaluators that require a ground_truth / expected_output field. _GROUND_TRUTH_EVALUATORS: set[str] = { "builtin.similarity", @@ -161,6 +170,7 @@ def _build_testing_criteria( model: str, *, include_data_mapping: bool = False, + include_tool_definitions: bool = False, ) -> list[dict[str, Any]]: """Build ``testing_criteria`` for ``evals.create()``. @@ -169,6 +179,8 @@ def _build_testing_criteria( model: Model deployment for the LLM judge. include_data_mapping: Whether to include field-level data mapping (required for the JSONL data source, not needed for response-based). + include_tool_definitions: Whether the mapped data items include tool + definitions. """ criteria: list[dict[str, Any]] = [] for name in evaluators: @@ -203,7 +215,7 @@ def _build_testing_criteria( mapping["context"] = "{{item.context}}" if qualified in _GROUND_TRUTH_EVALUATORS: mapping["ground_truth"] = "{{item.ground_truth}}" - if qualified in _TOOL_EVALUATORS: + if include_tool_definitions and qualified in _TOOL_DEFINITION_EVALUATORS: mapping["tool_definitions"] = "{{item.tool_definitions}}" entry["data_mapping"] = mapping @@ -713,6 +725,7 @@ class FoundryEvals: evaluators, self._model, include_data_mapping=True, + include_tool_definitions=has_tools, ), ) diff --git a/python/packages/foundry/tests/test_foundry_evals.py b/python/packages/foundry/tests/test_foundry_evals.py index 937a3cf524..a5d9f2e864 100644 --- a/python/packages/foundry/tests/test_foundry_evals.py +++ b/python/packages/foundry/tests/test_foundry_evals.py @@ -745,7 +745,12 @@ class TestBuildTestingCriteria: assert "conversation" not in criteria[1]["data_mapping"] def test_tool_evaluator_includes_tool_definitions(self) -> None: - criteria = _build_testing_criteria(["relevance", "tool_call_accuracy"], "gpt-4o", include_data_mapping=True) + criteria = _build_testing_criteria( + ["relevance", "tool_call_accuracy"], + "gpt-4o", + include_data_mapping=True, + include_tool_definitions=True, + ) # relevance: string query/response assert criteria[0]["data_mapping"]["query"] == "{{item.query}}" assert criteria[0]["data_mapping"]["response"] == "{{item.response}}" @@ -762,6 +767,17 @@ class TestBuildTestingCriteria: assert c["data_mapping"]["query"] == "{{item.query_messages}}", f"{c['name']}" assert c["data_mapping"]["response"] == "{{item.response_messages}}", f"{c['name']}" + def test_agent_evaluators_include_tool_definitions_when_tools_present(self) -> None: + agent_evals = ["task_adherence", "intent_resolution", "task_completion", "task_navigation_efficiency"] + criteria = _build_testing_criteria( + agent_evals, + "gpt-4o", + include_data_mapping=True, + include_tool_definitions=True, + ) + for c in criteria: + assert c["data_mapping"]["tool_definitions"] == "{{item.tool_definitions}}", f"{c['name']}" + def test_quality_evaluators_use_strings(self) -> None: quality_evals = ["coherence", "relevance", "fluency"] criteria = _build_testing_criteria(quality_evals, "gpt-4o", include_data_mapping=True) @@ -781,7 +797,12 @@ class TestBuildTestingCriteria: "tool_output_utilization", "tool_call_success", ] - criteria = _build_testing_criteria(tool_evals, "gpt-4o", include_data_mapping=True) + criteria = _build_testing_criteria( + tool_evals, + "gpt-4o", + include_data_mapping=True, + include_tool_definitions=True, + ) for c in criteria: assert "tool_definitions" in c["data_mapping"], f"{c['name']} missing tool_definitions"