diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 627b46e..1ab23cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,16 +18,17 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: - python-version: ${{ matrix.python-version }} + python-version: '3.13' enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions + cache-suffix: lint - - run: uv sync --frozen --all-groups + - run: uv sync --frozen --all-groups --all-extras - uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 # v3.0.0 with: @@ -43,8 +44,16 @@ jobs: fail-fast: false matrix: python-version: ['3.10', '3.12', '3.13'] + install: + - name: slim + extras: '' + - name: all-extras + extras: '--all-extras' + name: test on ${{ matrix.python-version }} (${{ matrix.install.name }}) + env: + COVERAGE_FILE: coverage-data/.coverage.${{ matrix.python-version }}-${{ matrix.install.name }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -53,12 +62,41 @@ jobs: python-version: ${{ matrix.python-version }} enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions - - run: uv sync --frozen --all-groups - - run: make testcov + - run: uv sync --frozen --group dev ${{ matrix.install.extras }} + - run: mkdir -p coverage-data + - run: uv run coverage run -m pytest + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-${{ matrix.python-version }}-${{ matrix.install.name }} + path: coverage-data/.coverage.* + include-hidden-files: true + + coverage: + needs: [test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + python-version: '3.13' + enable-cache: true # zizmor: ignore[cache-poisoning] -- Job does not produce release artifacts and does not have sensitive permissions + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: coverage-* + path: coverage-data + merge-multiple: true + + - run: uv sync --frozen --group dev --all-extras + - run: uv run coverage combine coverage-data + - run: uv run coverage report check: if: always() - needs: [lint, test] + needs: [lint, test, coverage] runs-on: ubuntu-latest steps: - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 @@ -74,7 +112,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -84,4 +122,4 @@ jobs: enable-cache: false - run: uv build - - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/AGENTS.md b/AGENTS.md index 7fd41db..0371d31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,15 +71,18 @@ Always run `make lint && make typecheck && make test` before committing. ## File structure ``` -src/pydantic_harness/ +pydantic_harness/ __init__.py # public API re-exports - guardrails.py # guardrail capabilities (InputGuardrail, OutputGuardrail, CostGuard, ToolGuard, AsyncGuardrail) - .py # each capability gets its own module + / # each capability gets its own package + __init__.py # public exports for the capability + _capability.py # capability class (AbstractCapability subclass) + _toolset.py # toolset implementation tests/ conftest.py # shared fixtures (TestModel, test_agent) - test_.py # tests mirror source modules -examples/ - / # runnable examples per capability + _/ # tests mirror source packages + test_.py +/ + README.md # standalone docs for the capability ``` ## Testing patterns diff --git a/Makefile b/Makefile index 84e9593..6053dde 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +.DEFAULT_GOAL := all + .PHONY: .uv .prek install format lint typecheck test testcov all .uv: @@ -7,12 +9,12 @@ @prek --version || echo 'Please install prek: https://github.com/j178/pre-commit-rs' install: .uv .prek - uv sync --frozen --all-groups + uv sync --frozen --all-extras --group lint prek install --install-hooks format: uv run ruff format - uv run ruff check --fix + uv run ruff check --fix --fix-only lint: uv run ruff format --check diff --git a/README.md b/README.md index 83cfea2..32c4b0e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Extras for specific capabilities: uv add "pydantic-harness[code-mode]" # CodeMode (adds the Monty sandbox) ``` -Requires Python 3.10+ and `pydantic-ai-slim>=1.76.0`. +Requires Python 3.10+ and `pydantic-ai-slim>=1.78.0`. ## Quick start diff --git a/demo_code_mode.py b/demo_code_mode.py new file mode 100644 index 0000000..b63aa41 --- /dev/null +++ b/demo_code_mode.py @@ -0,0 +1,123 @@ +"""End-to-end CodeMode demo against Anthropic Claude Sonnet 4.6. + +Run with: + + uv pip install anthropic logfire + ANTHROPIC_API_KEY=... LOGFIRE_TOKEN=... uv run python demo_code_mode.py + +Both `anthropic` and `logfire` are optional. Without `LOGFIRE_TOKEN` Logfire +prints structured spans to stdout instead of streaming to the UI. +""" + +from __future__ import annotations + +import asyncio +import os +import sys + +from pydantic_ai import Agent +from pydantic_ai.messages import ToolCallPart + +from pydantic_harness import CodeMode + +try: + import logfire +except ImportError: # pragma: no cover - logfire is optional + logfire = None # type: ignore[assignment] + + +def _configure_logfire() -> None: + """Configure Logfire if available, otherwise do nothing.""" + if logfire is None: + return + logfire.configure( + service_name='pydantic-harness-demo', + send_to_logfire='if-token-present', + console=logfire.ConsoleOptions(verbose=True), + ) + logfire.instrument_pydantic_ai() + try: + logfire.instrument_anthropic() # pyright: ignore[reportUnknownMemberType] + except Exception: # pragma: no cover - anthropic instrumentation is optional + pass + + +PRODUCTS: dict[str, dict[str, float | int]] = { + 'apple': {'price': 1.20, 'stock': 50}, + 'banana': {'price': 0.50, 'stock': 200}, + 'cherry': {'price': 3.00, 'stock': 12}, + 'date': {'price': 5.50, 'stock': 8}, + 'elderberry': {'price': 7.25, 'stock': 4}, +} + + +def get_price(item: str) -> float: + """Look up the unit price of an item in dollars.""" + if item not in PRODUCTS: + raise ValueError(f'Unknown item: {item!r}. Available: {sorted(PRODUCTS)}') + return float(PRODUCTS[item]['price']) + + +def get_stock(item: str) -> int: + """Return the current available stock for an item.""" + if item not in PRODUCTS: + raise ValueError(f'Unknown item: {item!r}. Available: {sorted(PRODUCTS)}') + return int(PRODUCTS[item]['stock']) + + +def apply_discount(amount: float, percent: int) -> float: + """Apply a percentage discount to a dollar amount and return the new total.""" + return round(amount * (1 - percent / 100), 2) + + +async def main() -> int: + """Run the CodeMode demo against Claude Sonnet 4.6.""" + if not os.environ.get('ANTHROPIC_API_KEY'): + print('ANTHROPIC_API_KEY not set', file=sys.stderr) + return 1 + + _configure_logfire() + + agent: Agent[None, str] = Agent( + 'anthropic:claude-sonnet-4-6', + capabilities=[CodeMode[None]()], + instructions=( + 'You are a shopping assistant. When the user asks a question that requires ' + 'multiple tool calls or arithmetic, use the `run_code` tool to write a single ' + 'Python snippet that calls the available functions (remember to `await` them) ' + 'and prints the final answer. Then summarise the result for the user.' + ), + ) + + agent.tool_plain(get_price) + agent.tool_plain(get_stock) + agent.tool_plain(apply_discount) + + question = ( + "I'd like to buy 4 apples, 3 bananas, 2 cherries, and 1 date. " + 'For any item where I am ordering more than 25% of available stock, ' + 'apply a 15% discount on that item only. ' + 'What is my total in dollars?' + ) + + if logfire is not None: + with logfire.span('code_mode_demo', question=question): + result = await agent.run(question) + else: + result = await agent.run(question) + + for msg in result.all_messages(): + for part in msg.parts: + if isinstance(part, ToolCallPart) and part.tool_name == 'run_code': + args = part.args if isinstance(part.args, dict) else None + if args and 'code' in args: + print(args['code']) + print() + + print(result.output) + + return 0 + + +if __name__ == '__main__': + sys.exit(asyncio.run(main())) diff --git a/pydantic_harness/__init__.py b/pydantic_harness/__init__.py new file mode 100644 index 0000000..d0f0067 --- /dev/null +++ b/pydantic_harness/__init__.py @@ -0,0 +1,16 @@ +"""Agent harness for composable, reusable AI agent capabilities, for Pydantic AI.""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .code_mode import CodeMode + +__all__ = ['CodeMode'] + + +def __getattr__(name: str) -> object: + if name == 'CodeMode': + from .code_mode import CodeMode + + return CodeMode + raise AttributeError(f'module {__name__!r} has no attribute {name!r}') diff --git a/pydantic_harness/code_mode/__init__.py b/pydantic_harness/code_mode/__init__.py new file mode 100644 index 0000000..8a267a4 --- /dev/null +++ b/pydantic_harness/code_mode/__init__.py @@ -0,0 +1,6 @@ +"""Code mode capability: route tool calls through a sandboxed Python environment.""" + +from pydantic_harness.code_mode._capability import CodeMode +from pydantic_harness.code_mode._toolset import CodeModeToolset + +__all__ = ['CodeMode', 'CodeModeToolset'] diff --git a/pydantic_harness/code_mode/_capability.py b/pydantic_harness/code_mode/_capability.py new file mode 100644 index 0000000..84c2106 --- /dev/null +++ b/pydantic_harness/code_mode/_capability.py @@ -0,0 +1,57 @@ +"""Code mode capability that routes selected tools through a Monty sandbox.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +from pydantic_ai import AbstractToolset +from pydantic_ai.capabilities import AbstractCapability, CapabilityOrdering +from pydantic_ai.capabilities._tool_search import ToolSearch as _ToolSearch +from pydantic_ai.tools import AgentDepsT, ToolSelector + +from pydantic_harness.code_mode._toolset import CodeModeToolset + + +@dataclass +class CodeMode(AbstractCapability[AgentDepsT]): + """Capability that exposes selected tools as callables inside a `run_code` sandbox. + + By default (`tools='all'`) every tool the agent has is wrapped behind a single + `run_code` tool -- the model writes Python that calls them as functions instead + of issuing tool calls directly. + + Pass a list of tool names or a callable predicate to `tools` to split the + toolset: matching tools become callables inside the sandbox, and the rest + stay visible to the model as normal tool calls. + + ```python + from pydantic_ai import Agent + from pydantic_harness import CodeMode + + # Sandbox all tools + agent = Agent('openai:gpt-5', capabilities=[CodeMode()]) + + # Sandbox only specific tools + agent = Agent('openai:gpt-5', capabilities=[CodeMode(tools=['search', 'fetch'])]) + ``` + """ + + tools: ToolSelector[AgentDepsT] = field(default='all') + """Which wrapped tools should be sandboxed inside `run_code`. + + - `'all'` (default): every tool the agent has is sandboxed. + - `Sequence[str]`: only tools whose names are listed are sandboxed. + - Callable `(ctx, tool_def) -> bool | Awaitable[bool]`: tools where the + callable returns `True` are sandboxed; the rest stay as native tool calls. + """ + + max_retries: int = 3 + """Maximum number of retries for the `run_code` tool (syntax errors count as retries).""" + + def get_ordering(self) -> CapabilityOrdering: + """CodeMode wraps around ToolSearch so that search_tools stays native.""" + return CapabilityOrdering(position='outermost', wraps=[_ToolSearch]) + + def get_wrapper_toolset(self, toolset: AbstractToolset[AgentDepsT]) -> AbstractToolset[AgentDepsT] | None: + """Wrap the agent's assembled toolset, splitting it into native + sandboxed subsets if needed.""" + return CodeModeToolset(wrapped=toolset, tool_selector=self.tools, max_retries=self.max_retries) diff --git a/pydantic_harness/code_mode/_toolset.py b/pydantic_harness/code_mode/_toolset.py new file mode 100644 index 0000000..b71d1e1 --- /dev/null +++ b/pydantic_harness/code_mode/_toolset.py @@ -0,0 +1,781 @@ +"""Code mode toolset that runs LLM-generated Python in a Monty sandbox.""" + +from __future__ import annotations + +import asyncio +import keyword +import re +import warnings +from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field, replace +from typing import Annotated, Any + +from pydantic import Field, TypeAdapter +from pydantic_ai import AbstractToolset, RunContext, ToolDefinition, WrapperToolset +from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry, UserError +from pydantic_ai.function_signature import FunctionSignature +from pydantic_ai.messages import ToolCallPart, ToolReturn, ToolReturnContent, ToolReturnPart, is_multi_modal_content +from pydantic_ai.tool_manager import ToolManager +from pydantic_ai.tools import AgentDepsT, ToolSelector, matches_tool_selector +from pydantic_ai.toolsets.abstract import SchemaValidatorProt, ToolsetTool + +try: + from pydantic_ai.toolsets._tool_search import _SEARCH_TOOLS_NAME # pyright: ignore[reportPrivateUsage] +except ImportError: # pragma: no cover + _SEARCH_TOOLS_NAME = 'search_tools' # pyright: ignore[reportConstantRedefinition] + +try: + from pydantic_monty import ( + ExternalException, + ExternalResult, + ExternalReturnValue, + FunctionSnapshot, + FutureSnapshot, + Monty, + MontyComplete, + MontyRepl, + MontyRuntimeError, + MontySyntaxError, + MontyTypingError, + NameLookupSnapshot, + ) +except ImportError as _import_error: # pragma: no cover + raise ImportError( + 'pydantic-monty is required for CodeMode. Install it with: pip install "pydantic-harness[code-mode]"' + ) from _import_error +from typing_extensions import NotRequired, TypedDict + +# Type alias for the dispatch callback passed to _execution_loop. +_DispatchFn = Callable[[str, dict[str, Any]], Coroutine[Any, Any, Any]] + + +class _RunCodeArguments(TypedDict): + code: Annotated[str, Field(description='The Python code to execute in the sandbox.')] + restart: NotRequired[ + Annotated[ + bool, + Field( + description='Set to true to reset REPL state. When false (default), state is preserved between calls.' + ), + ] + ] + + +_RUN_CODE_TOOL_NAME = 'run_code' +_RUN_CODE_ADAPTER = TypeAdapter(_RunCodeArguments) +_RUN_CODE_JSON_SCHEMA = _RUN_CODE_ADAPTER.json_schema() +_RUN_CODE_ARGS_VALIDATOR: SchemaValidatorProt = _RUN_CODE_ADAPTER.validator # pyright: ignore[reportAssignmentType] +# Used to serialize tool return values before sending into Monty (dump_python) +# and to reconstruct multimodal types (e.g. BinaryContent) from Monty results (validate_python). +_TOOL_RETURN_CONTENT_TA: TypeAdapter[Any] = TypeAdapter(ToolReturnContent) + +_RUN_CODE_BASE_DESCRIPTION = """\ +Write and run Python code in a sandboxed environment. + +The sandbox uses Monty, a subset of Python. Key restrictions: +- **No classes**: class definitions are not supported +- **No third-party libraries**: only the standard library modules listed below can be used +- **Importable standard library modules**: `sys`, `typing`, `asyncio`, `math`, `json`, `re`, `datetime`, `os`, `pathlib`. These must be imported at the top of your snippet before use, just like in regular Python. For example: `import asyncio` then `results = await asyncio.gather(tool_one(...), tool_two(...))`. +- **No `import *`**: wildcard imports are not supported + +State is preserved between calls (REPL-style). Set `restart: true` to reset state. + +The last expression's value is automatically captured as the return value — you do **not** need to \ +`print()` it. Avoid `print()` for return values as it produces Python string representations, not \ +structured data. Use `print()` only for supplementary logging or debug output. + +Returns the last expression's value directly. If `print()` was also called, returns \ +`{"output": "", "result": }`.\ +""" + + +def _functions_header(*, has_sync: bool, has_async: bool) -> str: + """Build the functions-header paragraph for the `run_code` tool description.""" + base = ( + '\nThe following functions are available inside the sandbox. Call them directly ' + '(do **not** redefine or import them). All parameters are keyword-only.' + ) + if has_async and not has_sync: + return base + ( + ' All tool functions are async: invoke them with `await`,' + ' e.g. `result = await tool_name(arg=value)`.' + ' Calling without `await` returns an unresolved future, not the value.' + ) + if has_sync and not has_async: + return base + (' All tool functions are synchronous: call them directly, e.g. `result = tool_name(arg=value)`.') + return base + ( + ' Async functions (`async def`) must be invoked with `await`,' + ' e.g. `result = await tool_name(arg=value)`.' + ' Sync functions (`def`) are called directly, e.g. `result = tool_name(arg=value)`.' + ) + + +_SEARCH_TOOLS_MODIFIER = ( + ' Note: discovered tools become callable as functions inside the run_code sandbox in subsequent invocations.' +) + +_TOOL_SEARCH_ADDENDUM = ( + f'\n\nNot all functions may be available initially.' + f' Use the `{_SEARCH_TOOLS_NAME}` tool to discover additional functions' + f' that will become callable in subsequent `run_code` invocations.' +) + +_INVALID_IDENT_CHARS = re.compile(r'[^a-zA-Z0-9_]') + + +def _sanitize_tool_name(name: str) -> str: + """Turn a tool name into a valid Python identifier. + + Replaces hyphens, dots, and other non-identifier characters with underscores, + prepends `_` if the result starts with a digit, appends `_` if it is a Python keyword. + """ + sanitized = _INVALID_IDENT_CHARS.sub('_', name) + if sanitized and sanitized[0].isdigit(): + sanitized = f'_{sanitized}' + if keyword.iskeyword(sanitized): + sanitized = f'{sanitized}_' + return sanitized or '_' + + +@dataclass(kw_only=True) +class _RunCodeTool(ToolsetTool[AgentDepsT]): + """ToolsetTool subclass that caches data computed during `get_tools`. + + Avoids a redundant `get_tools` call in `call_tool` by storing the + callable tool definitions and name mapping on the tool instance itself. + Follows the same pattern as `_SearchTool` in pydantic-ai's + `ToolSearchToolset`. + """ + + callable_defs: dict[str, ToolDefinition] + """Tool definitions callable from inside the sandbox, keyed by (possibly sanitized) name.""" + + sanitized_to_original: dict[str, str] + """Maps sanitized Python-safe names back to original tool names (only for renamed tools).""" + + wrapped_tools: dict[str, ToolsetTool[AgentDepsT]] + """The wrapped toolset's tools, keyed by original name.""" + + +@dataclass +class CodeModeToolset(WrapperToolset[AgentDepsT]): + """Implementation toolset for the `CodeMode` capability. + + Exposes a single `run_code` tool alongside any native (non-sandboxed) tools. + Tools selected by `tool_selector` are presented to the model as Python + function signatures inside the `run_code` tool description and become + callable from the sandbox at runtime. Non-selected tools remain visible + to the model as normal tool calls. + + Tools that require deferred execution (kind `external`/`unapproved`) or + deferred loading (`defer_loading=True`) cannot be called from inside the + sandbox and are dropped with a one-time `UserWarning`. + """ + + tool_selector: ToolSelector[AgentDepsT] = 'all' + """Which wrapped tools to sandbox inside `run_code`. Non-matching tools + are exposed as native tools.""" + + max_retries: int = 3 + """Maximum number of retries for the `run_code` tool (syntax errors count as retries).""" + + # init=False so `replace()` in `for_run` produces a fresh instance with _repl=None, + # giving each agent run isolated REPL state. Lazy-initialized on first call_tool. + _repl: MontyRepl | None = field(default=None, init=False, repr=False) + + # Tracks deferred-tool names we've already warned about so we don't spam the + # logs every step. Reset on `for_run` because each run gets a fresh instance. + _warned_deferred: set[str] = field(default_factory=set[str], init=False, repr=False) + + async def for_run(self, ctx: RunContext[AgentDepsT]) -> AbstractToolset[AgentDepsT]: + """Return a fresh toolset instance with isolated REPL state for this agent run.""" + wrapped = await self.wrapped.for_run(ctx) + return replace(self, wrapped=wrapped) + + async def for_run_step(self, ctx: RunContext[AgentDepsT]) -> AbstractToolset[AgentDepsT]: + """Update the wrapped toolset for this step while preserving REPL state.""" + new_wrapped = await self.wrapped.for_run_step(ctx) + if new_wrapped is self.wrapped: + return self + new_self = replace(self, wrapped=new_wrapped) + new_self._repl = self._repl + new_self._warned_deferred = self._warned_deferred + return new_self + + async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]: + """Return the `run_code` tool plus any native (non-sandboxed) tools.""" + wrapped_tools = await self.wrapped.get_tools(ctx) + + # Split tools into sandboxed vs native based on the selector. + # The search_tools tool (from ToolSearchToolset) is always kept native + # so the model can discover deferred tools alongside run_code. + sandboxed_tools: dict[str, ToolsetTool[AgentDepsT]] = {} + native_tools: dict[str, ToolsetTool[AgentDepsT]] = {} + for name, tool in wrapped_tools.items(): + if name == _SEARCH_TOOLS_NAME: + native_tools[name] = tool + elif await matches_tool_selector(self.tool_selector, ctx, tool.tool_def): + sandboxed_tools[name] = tool + else: + native_tools[name] = tool + + callable_defs, sanitized_to_original, native_fallbacks = self._partition_callable_tools(sandboxed_tools) + + # Tools that matched the selector but can't run in the sandbox (deferred + # execution, deferred loading) are promoted back to native tools so + # they remain visible to the model. + for name in native_fallbacks: + native_tools[name] = sandboxed_tools[name] + + description = self._build_description(callable_defs) + + if _RUN_CODE_TOOL_NAME in native_tools: + raise UserError( + f"Tool name '{_RUN_CODE_TOOL_NAME}' is reserved for code mode. Rename your tool to avoid conflicts." + ) + + # When search_tools is present, append context about run_code to its + # description and add a discovery note to the run_code description. + has_search_tools = _SEARCH_TOOLS_NAME in native_tools + if has_search_tools: + search_tool = native_tools[_SEARCH_TOOLS_NAME] + native_tools[_SEARCH_TOOLS_NAME] = replace( + search_tool, + tool_def=replace( + search_tool.tool_def, + description=(search_tool.tool_def.description or '') + _SEARCH_TOOLS_MODIFIER, + ), + ) + description += _TOOL_SEARCH_ADDENDUM + + result: dict[str, ToolsetTool[AgentDepsT]] = dict(native_tools) + result[_RUN_CODE_TOOL_NAME] = _RunCodeTool( + toolset=self, + tool_def=ToolDefinition( + name=_RUN_CODE_TOOL_NAME, + description=description, + parameters_json_schema=_RUN_CODE_JSON_SCHEMA, + metadata={'code_arg_name': 'code', 'code_arg_language': 'python'}, + sequential=True, + ), + max_retries=self.max_retries, + args_validator=_RUN_CODE_ARGS_VALIDATOR, + callable_defs=callable_defs, + sanitized_to_original=sanitized_to_original, + wrapped_tools=wrapped_tools, + ) + return result + + async def call_tool( + self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT] + ) -> Any: + """Execute Python code in the sandbox, or pass through to a native tool.""" + if not isinstance(tool, _RunCodeTool): + # Native (non-sandboxed) tool — pass through to the wrapped toolset. + return await self.wrapped.call_tool(name, tool_args, ctx, tool) + + code = tool_args['code'] + restart = tool_args.get('restart', False) + + # Clear the REPL on restart so that if type checking fails, the + # next retry still gets fresh_repl=True and is type-checked again. + if restart: + self._repl = None + fresh_repl = self._repl is None + + callable_defs = tool.callable_defs + sanitized_to_original = tool.sanitized_to_original + + # Build a ToolManager for the sandbox's inner tools so that sandboxed + # tool calls go through the standard validation/execution path. We + # inherit `root_capability` from the agent's ToolManager (for capability + # hooks) but use the *wrapped* toolset and its tools. + # See https://github.com/pydantic/pydantic-ai/pull/4307 + parent_tm = ctx.tool_manager + assert parent_tm is not None, 'CodeModeToolset requires ctx.tool_manager to be set' + tool_manager = ToolManager( + toolset=self.wrapped, + root_capability=parent_tm.root_capability, + ctx=ctx, + tools=tool.wrapped_tools, + ) + + # Determine execution mode for sandbox tool calls: + # - global_sequential: forced by durable execution engines (DBOS/Temporal) + # via the parallel execution mode context var. Checked with empty calls + # to isolate the context var from per-tool flags. + # - sequential_tools: per-tool `sequential` flags on ToolDefinition. + # These tools are rendered as `def` (sync) and resolved inline. + global_sequential = tool_manager.get_parallel_execution_mode([]) != 'parallel' + sequential_tools = {name for name, td in callable_defs.items() if td.sequential} + + # Collect nested tool calls and returns keyed by tool_call_id so they + # can be attached as metadata on the run_code ToolReturnPart. + nested_calls: dict[str, ToolCallPart] = {} + nested_returns: dict[str, ToolReturnPart] = {} + call_counter = 0 + + async def dispatch_tool_call(original_name: str, kwargs: dict[str, Any]) -> Any: + """Dispatch a single tool call from inside the sandbox. + + Returns the serialized tool result on success. On failure, the + exception propagates — the execution loop passes it back into + Monty via `ExternalException` so the sandbox sees it at the + `await` site. + """ + nonlocal call_counter + call_counter += 1 + parent_id = ctx.tool_call_id or 'pyd_ai_code_mode' + tool_call_id = f'{parent_id}__{call_counter}' + call_part = ToolCallPart(tool_name=original_name, args=kwargs, tool_call_id=tool_call_id) + nested_calls[tool_call_id] = call_part + + try: + result = await tool_manager.handle_call(call_part, wrap_validation_errors=False) + except (CallDeferred, ApprovalRequired) as e: + # Approval/deferral require a round-trip back to the caller, + # which the sandbox cannot do. Raise UserError so the execution + # loop passes it into Monty as an ExternalException; Monty + # re-raises it as MontyRuntimeError, which we catch and convert + # to ModelRetry. The error message is preserved through the chain. + raise UserError( + 'Tool approval and deferral are not supported in code mode. ' + f'Tool {original_name!r} raised {type(e).__name__}; ensure wrapped ' + 'tools do not use approval or deferral when used with CodeMode.' + ) from e + + # Unwrap ToolReturn to get the plain value for the sandbox, + # preserving the full ToolReturn metadata on the return part. + return_metadata: Any = None + if isinstance(result, ToolReturn): + return_metadata = result.metadata + result = result.return_value + + nested_returns[tool_call_id] = ToolReturnPart( + tool_name=original_name, + content=result, + tool_call_id=tool_call_id, + metadata=return_metadata, + ) + + # Serialize to JSON-compatible form so Monty receives only plain data. + return _TOOL_RETURN_CONTENT_TA.dump_python(result) + + # Static type checking on fresh REPL sessions (first call or after + # restart). Skipped on subsequent calls because accumulated REPL state + # (variables from prior snippets) is invisible to the stateless checker. + # Runs before REPL creation so that if this raises ModelRetry, the REPL + # stays None and the next retry still gets type-checked. + if fresh_repl and callable_defs: + self._type_check(code, callable_defs=callable_defs) + + # Create the REPL after type checking passes. + if fresh_repl: + self._repl = MontyRepl() + assert self._repl is not None + + capture = _PrintCapture() + + try: + monty_state = self._repl.feed_start(code, print_callback=capture) + completed = await _execution_loop( + monty_state, + dispatch=dispatch_tool_call, + callable_defs=callable_defs, + sanitized_to_original=sanitized_to_original, + sequential_tools=sequential_tools, + global_sequential=global_sequential, + ) + except MontySyntaxError as e: + raise ModelRetry(f'Syntax error in code:\n{_prepend_prints(e.display(), capture)}') from e + except MontyTypingError as e: # pragma: no cover — MontyRepl.feed_start doesn't raise this + raise ModelRetry(f'Type error in code:\n{_prepend_prints(e.display(), capture)}') from e + except MontyRuntimeError as e: + # Exceptions raised inside dispatch_tool_call (e.g. UserError from + # ApprovalRequired, or ModelRetry from a wrapped tool) are passed + # back into Monty via ExternalException. Monty re-raises them at the + # await site; if the sandbox code doesn't catch them, they bubble up + # as MontyRuntimeError. The original exception message is preserved + # in the display string, so the model sees a useful error. This means + # ModelRetry from a wrapped tool gets double-wrapped + # (ModelRetry → MontyRuntimeError → ModelRetry), but the retry + # semantics are the same — the model gets another chance. + raise ModelRetry(f'Runtime error:\n{_prepend_prints(e.display(), capture)}') from e + + result = completed.output + printed = capture.joined + + # Validate result to reconstruct multimodal types (e.g. BinaryContent from + # serialized dicts) so they flow through to the model natively. + if result is not None: + result = _TOOL_RETURN_CONTENT_TA.validate_python(result) + + # Build return value: + # - No print → return result directly (multimodal content stays top-level + # so _split_content can extract it for native model delivery) + # - Print + multimodal result → list format so _split_content can extract files + # - Print + plain result → dict with output/result keys + if not printed: + return_value: Any = result if result is not None else {} + elif result is None: + return_value = {'output': printed} + elif _contains_multimodal(result): + # Flatten lists so _split_content can find each multimodal item at top level. + return_value = [printed, *result] if isinstance(result, list) else [printed, result] + else: + return_value = {'output': printed, 'result': result} + + return ToolReturn( + return_value=return_value, + metadata={'code_mode': True, 'tool_calls': nested_calls, 'tool_returns': nested_returns}, + ) + + def _partition_callable_tools( + self, wrapped_tools: dict[str, ToolsetTool[AgentDepsT]] + ) -> tuple[dict[str, ToolDefinition], dict[str, str], set[str]]: + """Return tool definitions that can be called from inside the sandbox. + + Tool names that are not valid Python identifiers (e.g. MCP tools with + hyphens or dots like `get-weather`, `api.call`) are sanitized to + underscored forms and mapped back to their original names for dispatch. + + Tools requiring deferred execution (kind `external`/`unapproved`) or + deferred loading (`defer_loading=True`) cannot run in the sandbox and + are excluded from `callable_defs`. Their names are returned in the + third element so the caller can promote them back to native tools. + + Returns: + A tuple of `(callable_defs, sanitized_to_original, native_fallbacks)` + where `native_fallbacks` contains original tool names that should + be exposed as native tools instead of being sandboxed. + """ + callable_defs: dict[str, ToolDefinition] = {} + sanitized_to_original: dict[str, str] = {} + native_fallbacks: set[str] = set() + for name, tool in wrapped_tools.items(): + td = tool.tool_def + if td.defer: + if name not in self._warned_deferred: + self._warned_deferred.add(name) + warnings.warn( + f'CodeMode: tool {name!r} requires deferred execution ' + f'(kind={td.kind!r}) and cannot be called from inside the ' + f'sandbox; it will be exposed as a native tool instead.', + UserWarning, + stacklevel=2, + ) + native_fallbacks.add(name) + continue + if td.defer_loading: + if name not in self._warned_deferred: + self._warned_deferred.add(name) + warnings.warn( + f'CodeMode: tool {name!r} uses deferred loading (tool search) ' + f'and cannot be pre-registered in the sandbox; it will be ' + f'exposed as a native tool instead.', + UserWarning, + stacklevel=2, + ) + native_fallbacks.add(name) + continue + + safe_name = _sanitize_tool_name(name) + if safe_name == _RUN_CODE_TOOL_NAME: + raise UserError( + f"Tool name '{name}' (sanitized to '{safe_name}') conflicts with the code mode " + f'meta-tool. Rename your tool to avoid conflicts.' + ) + if safe_name in callable_defs: + existing = sanitized_to_original.get(safe_name, safe_name) + warnings.warn( + f'CodeMode: tool {name!r} (sanitized to {safe_name!r}) collides ' + f'with {existing!r}; {name!r} will be hidden from the sandbox.', + UserWarning, + stacklevel=2, + ) + continue + # Warn when a sandboxed tool has no return schema — the generated + # signature will show `-> Any`, giving the model no type information + # about the return shape, which limits code mode effectiveness. + if td.return_schema is None and name not in self._warned_deferred: + self._warned_deferred.add(name) + warnings.warn( + f'CodeMode: tool {name!r} has no return schema; ' + f'its signature will show `-> Any`, which may reduce code mode effectiveness.', + UserWarning, + stacklevel=2, + ) + + if safe_name != name: + sanitized_to_original[safe_name] = name + td = replace(td, name=safe_name) + + callable_defs[safe_name] = td + return callable_defs, sanitized_to_original, native_fallbacks + + @staticmethod + def _build_description(callable_defs: dict[str, ToolDefinition]) -> str: + """Render the `run_code` description: base prose + TypedDicts + function signatures.""" + if not callable_defs: + return _RUN_CODE_BASE_DESCRIPTION + + sigs, conflicting = _get_sigs_and_conflicting(callable_defs) + type_blocks = FunctionSignature.render_type_definitions(sigs, conflicting) + function_blocks = [ + td.render_signature('...', is_async=not td.sequential, conflicting_type_names=conflicting) + for td in callable_defs.values() + ] + + has_sync = any(td.sequential for td in callable_defs.values()) + has_async = any(not td.sequential for td in callable_defs.values()) + header = _functions_header(has_sync=has_sync, has_async=has_async) + + sections = [_RUN_CODE_BASE_DESCRIPTION, header] + if type_blocks: + sections.append('```python\n' + '\n\n'.join(type_blocks) + '\n```') + sections.append('```python\n' + '\n\n'.join(function_blocks) + '\n```') + return '\n\n'.join(sections) + + @staticmethod + def _build_type_check_stubs(callable_defs: dict[str, ToolDefinition]) -> str: + """Build Python stubs for Monty's static type checker.""" + sigs, conflicting = _get_sigs_and_conflicting(callable_defs) + parts = ['import asyncio\nfrom typing import Any, TypedDict, NotRequired, Literal'] + type_blocks = FunctionSignature.render_type_definitions(sigs, conflicting) + parts.extend(type_blocks) + parts.extend( + td.render_signature( + 'raise NotImplementedError()', is_async=not td.sequential, conflicting_type_names=conflicting + ) + for td in callable_defs.values() + ) + return '\n\n'.join(parts) + + @staticmethod + def _type_check(code: str, *, callable_defs: dict[str, ToolDefinition]) -> None: + """Type-check a code snippet against tool signatures before execution. + + Uses Monty's stateless type checker with function stubs. Only sound + when the REPL has no accumulated state (first call or after restart). + + Raises: + ModelRetry: If the code has type errors or syntax errors. + """ + stubs = CodeModeToolset._build_type_check_stubs(callable_defs) + try: + Monty(code, type_check=True, type_check_stubs=stubs) + except MontyTypingError as e: + raise ModelRetry(f'Type error in code:\n{e.display()}') from e + except MontySyntaxError as e: + raise ModelRetry(f'Syntax error in code:\n{e.display()}') from e + + +def _get_sigs_and_conflicting( + callable_defs: dict[str, ToolDefinition], +) -> tuple[list[FunctionSignature], frozenset[str]]: + """Extract FunctionSignatures and conflicting type names from tool definitions.""" + sigs: list[FunctionSignature] = [] + for td in callable_defs.values(): + assert td.function_signature is not None, f'function_signature missing for tool {td.name!r}' + sigs.append(td.function_signature) + return sigs, FunctionSignature.get_conflicting_type_names(sigs) + + +async def _execution_loop( + monty_state: FunctionSnapshot | FutureSnapshot | NameLookupSnapshot | MontyComplete, + *, + dispatch: _DispatchFn, + callable_defs: dict[str, ToolDefinition], + sanitized_to_original: dict[str, str], + sequential_tools: set[str], + global_sequential: bool, +) -> MontyComplete: + """Drive the Monty REPL via the synchronous snapshot API until completion. + + Uses Monty's `feed_start`/`resume` snapshot API instead of `feed_run_async` + to avoid background threads and `call_soon_threadsafe`. This makes it safe + to run inside restricted event loops like Temporal's workflow sandbox. + + Tool calls are handled based on their execution mode: + + - **Parallel tools** (``async def``): deferred via ``resume(future=...)`` + and eagerly scheduled as ``asyncio.Task``s for concurrent execution. + Resolved at ``FutureSnapshot`` via ``asyncio.gather``. + - **Sequential tools** (``def``): resolved inline at ``FunctionSnapshot`` + via ``resume(return_value=...)`` or ``resume(exception=...)``. Before + dispatching, any pending parallel tasks are awaited to maintain ordering. + - **Global sequential mode** (DBOS/Temporal): all tools are deferred via + ``resume(future=...)`` but stored as bare coroutines and awaited + one-at-a-time at ``FutureSnapshot`` to prevent interleaving. + """ + pending: dict[int, asyncio.Task[Any] | Coroutine[Any, Any, Any]] = {} + # Results from parallel tasks that were awaited early (at a sequential-tool + # barrier) but whose FutureSnapshot hasn't been reached yet. + pre_resolved: dict[int, ExternalResult] = {} + try: + while not isinstance(monty_state, MontyComplete): + if isinstance(monty_state, NameLookupSnapshot): + monty_state = monty_state.resume() + elif isinstance(monty_state, FunctionSnapshot): + monty_state = await _handle_function_snapshot( + monty_state, + dispatch, + callable_defs, + sanitized_to_original, + sequential_tools=sequential_tools, + global_sequential=global_sequential, + pending=pending, + pre_resolved=pre_resolved, + ) + else: + monty_state = await _resolve_future_snapshot( + monty_state, + pending=pending, + pre_resolved=pre_resolved, + global_sequential=global_sequential, + ) + finally: + for item in pending.values(): # pragma: no cover + if isinstance(item, asyncio.Task): + item.cancel() + else: + item.close() + + return monty_state + + +async def _handle_function_snapshot( + snapshot: FunctionSnapshot, + dispatch: _DispatchFn, + callable_defs: dict[str, ToolDefinition], + sanitized_to_original: dict[str, str], + *, + sequential_tools: set[str], + global_sequential: bool, + pending: dict[int, asyncio.Task[Any] | Coroutine[Any, Any, Any]], + pre_resolved: dict[int, ExternalResult], +) -> FunctionSnapshot | FutureSnapshot | NameLookupSnapshot | MontyComplete: + """Handle a single FunctionSnapshot from the Monty execution loop.""" + fn_name = snapshot.function_name + + if fn_name not in callable_defs: + return snapshot.resume(exception=NameError(f'Unknown function: {fn_name}')) + + if snapshot.args: + return snapshot.resume( + exception=TypeError(f'{fn_name}() does not accept positional arguments; use keyword arguments') + ) + + original_name = sanitized_to_original.get(fn_name, fn_name) + + if fn_name in sequential_tools: + # Per-tool sequential: rendered as `def` (sync), so must resolve inline — + # the sandbox code doesn't `await` the result. Await pending parallel + # tasks first (barrier) to maintain ordering. + for cid in list(pending): + pre_resolved[cid] = await _resolve_coro(pending.pop(cid)) + outcome = await _resolve_coro(dispatch(original_name, snapshot.kwargs)) + if 'return_value' in outcome: + return snapshot.resume(return_value=outcome['return_value']) + return snapshot.resume(exception=outcome['exception']) + + # Deferred execution — store for later resolution at FutureSnapshot. + if global_sequential: + # Bare coroutine — don't schedule on the event loop yet. + pending[snapshot.call_id] = dispatch(original_name, snapshot.kwargs) + else: + # Eagerly schedule as a Task for concurrent execution. + pending[snapshot.call_id] = asyncio.ensure_future(dispatch(original_name, snapshot.kwargs)) + return snapshot.resume(future=...) + + +async def _resolve_future_snapshot( + snapshot: FutureSnapshot, + *, + pending: dict[int, asyncio.Task[Any] | Coroutine[Any, Any, Any]], + pre_resolved: dict[int, ExternalResult], + global_sequential: bool, +) -> FunctionSnapshot | FutureSnapshot | NameLookupSnapshot | MontyComplete: + """Resolve pending tool calls at a FutureSnapshot.""" + pending_ids = snapshot.pending_call_ids + if not pending_ids: # pragma: no cover + return snapshot.resume(results={}) + + results: dict[int, ExternalResult] = {} + for cid in pending_ids: + if cid in pre_resolved: + results[cid] = pre_resolved.pop(cid) + elif global_sequential: + results[cid] = await _resolve_coro(pending.pop(cid)) + + # Gather remaining parallel tasks. + gather_ids = [cid for cid in pending_ids if cid not in results] + if gather_ids: + tasks = [pending[cid] for cid in gather_ids] + settled = await asyncio.gather(*tasks, return_exceptions=True) + for cid in gather_ids: + del pending[cid] + for cid, outcome in zip(gather_ids, settled): + results[cid] = _settle_outcome(outcome) + + return snapshot.resume(results=results) + + +async def _resolve_coro( + coro: Coroutine[Any, Any, Any] | asyncio.Task[Any], +) -> ExternalReturnValue | ExternalException: + """Await a single coroutine/task and wrap the result for Monty.""" + try: + result = await coro + except Exception as exc: + return ExternalException(exception=exc) + else: + return ExternalReturnValue(return_value=result) + + +def _settle_outcome(outcome: Any) -> ExternalReturnValue | ExternalException: + """Wrap an `asyncio.gather(return_exceptions=True)` outcome for Monty.""" + if isinstance(outcome, Exception): + return ExternalException(exception=outcome) + if isinstance(outcome, BaseException): # pragma: no cover + raise outcome + return ExternalReturnValue(return_value=outcome) + + +def _prepend_prints(error_message: str, capture: _PrintCapture) -> str: + """Prepend any captured print output to an error message. + + When sandbox code prints debug output before crashing, this preserves + that output in the error so the model can use it for debugging. + """ + printed = capture.joined.rstrip('\n') + if not printed: + return error_message + return f'[stdout before error]\n{printed}\n[/stdout before error]\n{error_message}' + + +def _contains_multimodal(value: Any) -> bool: + """Check if a value is or directly contains multimodal content (images, audio, etc.).""" + if is_multi_modal_content(value): + return True + if isinstance(value, list): + return any(is_multi_modal_content(item) for item in value) # pyright: ignore[reportUnknownVariableType] + return False + + +class _PrintCapture: + """Accumulates print-callback chunks from the Monty REPL. + + Pulled out to module scope (rather than a closure inside `call_tool`) so + the callback path is testable in isolation and visible to coverage.py. + """ + + def __init__(self) -> None: + self._chunks: list[str] = [] + + def __call__(self, _stream: str, text: str) -> None: + self._chunks.append(text) + + @property + def joined(self) -> str: + return ''.join(self._chunks) diff --git a/src/pydantic_harness/py.typed b/pydantic_harness/py.typed similarity index 100% rename from src/pydantic_harness/py.typed rename to pydantic_harness/py.typed diff --git a/pyproject.toml b/pyproject.toml index 40261d2..8d2827c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,20 @@ classifiers = [ 'Topic :: Software Development :: Libraries', 'Typing :: Typed', ] -dependencies = ['pydantic-ai-slim>=1.76.0'] +dependencies = [ + 'pydantic-ai-slim>=1.78.0', +] + +[project.optional-dependencies] +code-mode = [ + 'pydantic-monty>=0.0.10', +] +temporal = [ + 'pydantic-ai-slim[temporal]', +] +dbos = [ + 'pydantic-ai-slim[dbos]', +] [project.urls] Homepage = 'https://github.com/pydantic/pydantic-harness' @@ -35,16 +48,21 @@ Issues = 'https://github.com/pydantic/pydantic-harness/issues' [dependency-groups] dev = [ + 'pydantic-harness[code-mode]', 'pytest', 'anyio[trio]', 'pytest-anyio', 'coverage', + 'logfire[httpx]>=4.31.0', ] lint = [ 'ruff>=0.14', 'pyright>=1.1.408', ] +[tool.uv.sources] +pydantic-ai-slim = { git = 'https://github.com/pydantic/pydantic-ai.git', branch = 'main', subdirectory = 'pydantic_ai_slim' } + [tool.hatch.version] source = 'uv-dynamic-versioning' @@ -53,8 +71,9 @@ vcs = 'git' style = 'pep440' bump = true + [tool.hatch.build.targets.wheel] -packages = ['src/pydantic_harness'] +packages = ['pydantic_harness'] [tool.ruff] line-length = 120 @@ -90,18 +109,26 @@ executionEnvironments = [ [tool.pytest.ini_options] testpaths = ['tests'] xfail_strict = true -filterwarnings = ['error'] +filterwarnings = [ + 'error', + # DBOS's run_sync triggers this on Python 3.12+ — not our code. + 'ignore:There is no current event loop:DeprecationWarning', +] anyio_mode = 'auto' [tool.coverage.run] branch = true source = ['pydantic_harness', 'tests'] +[tool.coverage.paths] +source = ['.', '/home/runner/work/pydantic-harness/pydantic-harness'] + [tool.coverage.report] fail_under = 100 show_missing = true exclude_lines = [ 'pragma: no cover', + 'pragma: lax no cover', 'assert_never', 'if TYPE_CHECKING:', ] diff --git a/src/pydantic_harness/__init__.py b/src/pydantic_harness/__init__.py deleted file mode 100644 index 9d728b6..0000000 --- a/src/pydantic_harness/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Agent harness for composable, reusable AI agent capabilities, built on PydanticAI. - -Usage: - from pydantic_harness import Memory, Skills, Guardrails, ... -""" - -# Each capability module is imported and re-exported here. -# Capabilities are listed alphabetically. - -__all__: list[str] = [] diff --git a/tests/_code_mode/__init__.py b/tests/_code_mode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/_code_mode/test_code_mode.py b/tests/_code_mode/test_code_mode.py new file mode 100644 index 0000000..8b3ebdf --- /dev/null +++ b/tests/_code_mode/test_code_mode.py @@ -0,0 +1,1649 @@ +"""Tests for the `CodeMode` capability and the `CodeModeToolset` it wraps. + +Style follows `pydantic_ai/tests/test_toolsets.py`: module-level +`pytestmark = pytest.mark.anyio`, an `anyio_backend` fixture, async tests, and a +`build_run_context` factory. The `anyio` package's pytest plugin is already +loaded by the project (no extra dev dependency needed). +""" + +from __future__ import annotations + +from typing import Any, TypeVar + +import pytest +from pydantic_ai import ( + AbstractToolset, + Agent, + RunContext, + Tool, + ToolDefinition, +) +from pydantic_ai.exceptions import ModelRetry +from pydantic_ai.models.test import TestModel +from pydantic_ai.toolsets.abstract import ToolsetTool +from pydantic_ai.toolsets.function import FunctionToolset +from pydantic_ai.usage import RunUsage +from pydantic_core import SchemaValidator, core_schema +from typing_extensions import TypedDict + +from pydantic_harness import CodeMode +from pydantic_harness.code_mode import CodeModeToolset +from pydantic_harness.code_mode._toolset import ( # pyright: ignore[reportPrivateUsage] + _SEARCH_TOOLS_MODIFIER, + _TOOL_SEARCH_ADDENDUM, + _PrintCapture, + _sanitize_tool_name, +) + +pytestmark = pytest.mark.anyio + +T = TypeVar('T') + + +@pytest.fixture +def anyio_backend() -> str: + """Run async tests on the asyncio backend (matching upstream pydantic-ai).""" + return 'asyncio' + + +def build_run_context(deps: T, run_step: int = 0) -> RunContext[T]: + """Build a `RunContext` for invoking toolsets directly in tests. + + Mirrors the helper at `pydantic_ai/tests/test_toolsets.py`. + """ + return RunContext[T]( + deps=deps, + model=TestModel(), + usage=RunUsage(), + prompt=None, + messages=[], + run_step=run_step, + ) + + +async def build_ctx(deps: T, toolset: AbstractToolset[T], run_step: int = 0) -> RunContext[T]: + """Build a `RunContext` with a prepared `ToolManager`. + + Use this for tests that call `call_tool` — `CodeModeToolset` requires + `ctx.tool_manager` to be set. + """ + from pydantic_ai.tool_manager import ToolManager + + ctx = build_run_context(deps, run_step=run_step) + tm = ToolManager(toolset=toolset) + prepared_tm = await tm.for_run_step(ctx) + ctx.tool_manager = prepared_tm + return ctx + + +# --------------------------------------------------------------------------- +# Sample tool functions used by tests +# --------------------------------------------------------------------------- + + +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + +def greet(name: str, greeting: str = 'Hello') -> str: + """Greet someone.""" + return f'{greeting}, {name}!' + + +class Address(TypedDict): + """A simple postal address.""" + + street: str + city: str + + +class Person(TypedDict): + """A person with a home address.""" + + name: str + home: Address + + +def lookup_person(person: Person, count: int = 1) -> str: + """Look up details for a person.""" + return f'{count}x {person["name"]} @ {person["home"]["street"]}' + + +# Hand-built `ToolDefinition` objects + a tiny stub toolset are used by +# `test_conflicting_typed_dicts_get_tool_name_prefix` to exercise the +# `needs_prefix=True` rendering path. Going through Pydantic's JSON schema generator +# would not produce a true `$def`-key collision (Pydantic disambiguates `$def` keys +# by Python class identity even when `__name__` matches), so we build the schemas by +# hand and feed them through a fake toolset. + + +def _make_address_tool_def(name: str, description: str, addr_field: str) -> ToolDefinition: + """Build a `ToolDefinition` whose `$defs` contains an `Address` type with one field.""" + return ToolDefinition( + name=name, + description=description, + parameters_json_schema={ + 'type': 'object', + '$defs': { + 'Address': { + 'type': 'object', + 'title': 'Address', + 'properties': {addr_field: {'type': 'string'}}, + 'required': [addr_field], + }, + }, + 'properties': { + 'addr': {'$ref': '#/$defs/Address'}, + 'label': {'type': 'string'}, + }, + 'required': ['addr', 'label'], + }, + return_schema={'type': 'string'}, + ) + + +class _StaticToolset(AbstractToolset[None]): + """A minimal `AbstractToolset` that returns a fixed set of `ToolDefinition`s. + + Mirrors the `MockToolsetWithInstructions` pattern from `pydantic_ai/tests/test_toolsets.py`. + Used by tests that need to construct hand-crafted `ToolDefinition`s without going + through the function-introspection pipeline. + """ + + def __init__(self, tool_defs: list[ToolDefinition], results: dict[str, Any] | None = None) -> None: + self._tool_defs = tool_defs + self._results = results or {} + + @property + def id(self) -> str | None: + return None # pragma: no cover - required by AbstractToolset, never read in tests + + async def get_tools(self, ctx: RunContext[None]) -> dict[str, ToolsetTool[None]]: + return { + td.name: ToolsetTool( + toolset=self, + tool_def=td, + max_retries=1, + args_validator=_ANY_VALIDATOR, + ) + for td in self._tool_defs + } + + async def call_tool( + self, + name: str, + tool_args: dict[str, Any], + ctx: RunContext[None], + tool: ToolsetTool[None], + ) -> Any: + # Tests always set up `_results` for every tool name they invoke; the + # fallback exists only to keep the abstract contract satisfied. + return self._results[name] + + +_ANY_VALIDATOR = SchemaValidator(schema=core_schema.any_schema()) + + +def _build_function_toolset(*tools: Any) -> FunctionToolset[None]: + return FunctionToolset[None](tools=[Tool(t) for t in tools]) + + +# --------------------------------------------------------------------------- +# OTel / Logfire instrumentation (import block at module level) +# --------------------------------------------------------------------------- + +try: + from logfire.testing import CaptureLogfire + + logfire_installed = True +except ImportError: # pragma: no cover + logfire_installed = False + + +class TestCodeMode: + # --------------------------------------------------------------------------- + # `tools='all'` (default) behaviour + # --------------------------------------------------------------------------- + + async def test_default_wraps_all_tools_behind_run_code(self) -> None: + """`CodeMode()` exposes only `run_code` and renders every tool as an `async def`.""" + toolset = _build_function_toolset(add, greet) + wrapper = CodeMode[None]().get_wrapper_toolset(toolset) + assert isinstance(wrapper, CodeModeToolset) + + tools = await wrapper.get_tools(build_run_context(None)) + assert list(tools.keys()) == ['run_code'] + + description = tools['run_code'].tool_def.description + assert description is not None + assert 'async def add(*, a: int, b: int) -> int' in description + assert 'async def greet(*, name: str, greeting: str' in description + assert '"""Add two numbers."""' in description + # The base description must tell the model to await tool calls. + assert 'await' in description + + async def test_run_code_executes_call_through_monty(self) -> None: + """End-to-end: `run_code` runs Python in Monty and dispatches to a sync wrapped tool.""" + toolset = _build_function_toolset(add) + wrapper = CodeMode[None]().get_wrapper_toolset(toolset) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + result = await wrapper.call_tool( + 'run_code', + {'code': 'print(await add(a=2, b=3))'}, + ctx, + tools['run_code'], + ) + assert result.return_value == {'output': '5\n'} + + # Nested tool calls are recorded as ToolCallPart/ToolReturnPart pairs in metadata. + assert result.metadata['code_mode'] is True + calls = result.metadata['tool_calls'] + returns = result.metadata['tool_returns'] + assert list(calls.keys()) == ['pyd_ai_code_mode__1'] + assert calls['pyd_ai_code_mode__1'].tool_name == 'add' + assert calls['pyd_ai_code_mode__1'].args == {'a': 2, 'b': 3} + assert returns['pyd_ai_code_mode__1'].tool_name == 'add' + assert returns['pyd_ai_code_mode__1'].content == 5 + + async def test_run_code_executes_string_returning_tool_with_default_arg(self) -> None: + """End-to-end: a string-returning tool with a default arg is callable from the sandbox. + + Exercises (a) string return values flowing back through the await/dispatch loop, + (b) default-argument handling — the LLM-side code only passes `name`, not `greeting`. + """ + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(greet)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + result = await wrapper.call_tool( + 'run_code', + {'code': "print(await greet(name='Alice'))"}, + ctx, + tools['run_code'], + ) + assert result.return_value == {'output': 'Hello, Alice!\n'} + + async def test_run_code_can_chain_multiple_tool_calls_in_one_snippet(self) -> None: + """A realistic LLM snippet that calls two tools in one `run_code` invocation.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add, greet)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + code = "total = await add(a=2, b=3)\nmsg = await greet(name=str(total), greeting='Result is')\nprint(msg)" + result = await wrapper.call_tool('run_code', {'code': code}, ctx, tools['run_code']) + assert result.return_value == {'output': 'Result is, 5!\n'} + + async def test_run_code_parallel_tool_calls_via_gather(self) -> None: + """Concurrent tool calls via asyncio.gather work and record all nested metadata.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + code = 'import asyncio\nresults = await asyncio.gather(add(a=1, b=2), add(a=3, b=4))\nresults' + result = await wrapper.call_tool('run_code', {'code': code}, ctx, tools['run_code']) + assert result.return_value == [3, 7] + + # Both parallel calls are recorded in metadata. + calls = result.metadata['tool_calls'] + returns = result.metadata['tool_returns'] + assert len(calls) == 2 + assert len(returns) == 2 + + async def test_run_code_parallel_tool_calls_one_fails(self) -> None: + """When one of several parallel tool calls fails, the error surfaces as ModelRetry.""" + + def flaky(x: int) -> int: + """Always fails.""" + raise ModelRetry('not allowed') + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add, flaky)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + code = 'import asyncio\nawait asyncio.gather(add(a=1, b=2), flaky(x=3))' + with pytest.raises(ModelRetry, match='not allowed'): + await wrapper.call_tool('run_code', {'code': code}, ctx, tools['run_code']) + + async def test_run_code_renders_no_arg_tool_signature(self) -> None: + """A no-argument tool renders as `async def name() -> ...` (without `(*, ...)`). + + Covers the empty-params branch of `FunctionSignature._render` and verifies the + no-args path through Monty round-trips correctly. + """ + + def now_iso() -> str: + """Return a fake fixed timestamp.""" + return '2026-04-08T12:00:00Z' + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(now_iso)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + description = tools['run_code'].tool_def.description + assert description is not None + # Note the lack of `(*, ...)` — empty params render as `()`. + assert 'async def now_iso() -> str' in description + assert 'async def now_iso(*' not in description + + result = await wrapper.call_tool( + 'run_code', + {'code': 'print(await now_iso())'}, + ctx, + tools['run_code'], + ) + assert result.return_value == {'output': '2026-04-08T12:00:00Z\n'} + + async def test_run_code_state_persists_between_calls(self) -> None: + """REPL state must survive across consecutive `run_code` calls within a run.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + + first = await wrapper.call_tool('run_code', {'code': 'x = await add(a=1, b=2)'}, ctx, run_code) + assert first.return_value == {} # assignment, no output, no expression result + second = await wrapper.call_tool('run_code', {'code': 'print(x * 10)'}, ctx, run_code) + assert second.return_value == {'output': '30\n'} + + async def test_run_code_restart_resets_repl_state(self) -> None: + """Passing `restart=True` clears any previously-set names in the sandbox.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + + await wrapper.call_tool('run_code', {'code': 'x = 99'}, ctx, run_code) + # After restart, `x` should no longer exist — on a fresh REPL the static + # type checker catches undefined names before execution. + with pytest.raises(ModelRetry, match=r'x'): + await wrapper.call_tool('run_code', {'code': 'print(x)', 'restart': True}, ctx, run_code) + + async def test_run_code_returns_last_expression_value(self) -> None: + """When the last statement is an expression, its value is returned in `result`.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + result = await wrapper.call_tool('run_code', {'code': '1 + 2'}, ctx, tools['run_code']) + # No print output → result returned directly (not wrapped in a dict). + assert result.return_value == 3 + + async def test_run_code_syntax_error_becomes_model_retry(self) -> None: + """A Python syntax error is surfaced as `ModelRetry` so the model can fix it.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + # Fresh REPL: type checker catches the syntax error. + with pytest.raises(ModelRetry, match=r'Syntax error in code'): + await wrapper.call_tool('run_code', {'code': 'def ('}, ctx, run_code) + + # Non-fresh REPL: feed_start catches the syntax error at runtime. + await wrapper.call_tool('run_code', {'code': '1 + 1', 'restart': True}, ctx, run_code) + with pytest.raises(ModelRetry, match=r'Syntax error in code'): + await wrapper.call_tool('run_code', {'code': 'def ('}, ctx, run_code) + + # Non-fresh REPL: undefined name triggers NameLookupSnapshot → NameError. + with pytest.raises(ModelRetry, match=r"name 'undefined_var' is not defined"): + await wrapper.call_tool('run_code', {'code': 'print(undefined_var)'}, ctx, run_code) + + async def test_run_code_typing_error_becomes_model_retry(self) -> None: + """A `MontyTypingError` from static type checking is translated into `ModelRetry`. + + On a fresh REPL (first call or after restart), the code is type-checked + before execution using Monty's stateless type checker. + """ + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + with pytest.raises(ModelRetry, match=r'Type error in code'): + await wrapper.call_tool( + 'run_code', + {'code': '"hello" + 1'}, + ctx, + tools['run_code'], + ) + + # --------------------------------------------------------------------------- + # `for_run` / `for_run_step` lifecycle + # --------------------------------------------------------------------------- + + async def test_for_run_returns_fresh_instance_with_cleared_repl(self) -> None: + """`for_run` must hand back a new toolset instance — concurrent runs cannot share REPL state.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + + # Force lazy REPL creation on the *original* instance. + tools = await wrapper.get_tools(ctx) + await wrapper.call_tool('run_code', {'code': 'x = 1'}, ctx, tools['run_code']) + assert wrapper._repl is not None # pyright: ignore[reportPrivateUsage] + + fresh = await wrapper.for_run(ctx) + assert isinstance(fresh, CodeModeToolset) + assert fresh is not wrapper + assert fresh._repl is None # pyright: ignore[reportPrivateUsage] + + async def test_for_run_step_short_circuits_when_wrapped_unchanged(self) -> None: + """If the inner toolset doesn't change between steps, `for_run_step` returns `self` unchanged.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = build_run_context(None) + same = await wrapper.for_run_step(ctx) + assert same is wrapper + + async def test_for_run_step_preserves_repl_when_wrapped_changes(self) -> None: + """When the wrapped toolset changes between steps, REPL state must carry over to the new instance.""" + + class _SwappingToolset(AbstractToolset[None]): + """Returns a *different* underlying toolset on each `for_run_step` call.""" + + def __init__(self) -> None: + self._inner = _build_function_toolset(add) + self._step = 0 + + @property + def id(self) -> str | None: + return None # pragma: no cover - required by AbstractToolset, never read + + async def get_tools(self, ctx: RunContext[None]) -> dict[str, ToolsetTool[None]]: + return await self._inner.get_tools(ctx) + + async def call_tool( # pragma: no cover - test only exercises lifecycle methods, not call_tool + self, + name: str, + tool_args: dict[str, Any], + ctx: RunContext[None], + tool: ToolsetTool[None], + ) -> Any: + return await self._inner.call_tool(name, tool_args, ctx, tool) + + async def for_run_step(self, ctx: RunContext[None]) -> AbstractToolset[None]: + # Return a brand-new toolset on every step so `is` comparison fails in + # `CodeModeToolset.for_run_step`, forcing the rebuild branch. + self._step += 1 + new_self = _SwappingToolset() + new_self._step = self._step + return new_self + + wrapper = CodeMode[None]().get_wrapper_toolset(_SwappingToolset()) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + + # Lazily create the REPL on the original instance. + tools = await wrapper.get_tools(ctx) + await wrapper.call_tool('run_code', {'code': 'x = 7'}, ctx, tools['run_code']) + original_repl = wrapper._repl # pyright: ignore[reportPrivateUsage] + assert original_repl is not None + + next_step = await wrapper.for_run_step(ctx) + assert isinstance(next_step, CodeModeToolset) + assert next_step is not wrapper + # State carries over so the LLM doesn't lose its variables between steps. + assert next_step._repl is original_repl # pyright: ignore[reportPrivateUsage] + + # --------------------------------------------------------------------------- + # Filter behaviour + # --------------------------------------------------------------------------- + + async def test_filter_keeps_rejected_tools_native(self) -> None: + """A callable filter sandboxes accepted tools and leaves the rest visible to the model.""" + capability = CodeMode[None](tools=lambda ctx, td: td.name == 'add') + wrapper = capability.get_wrapper_toolset(_build_function_toolset(add, greet)) + assert isinstance(wrapper, CodeModeToolset) + + tools = await wrapper.get_tools(build_run_context(None)) + assert sorted(tools.keys()) == ['greet', 'run_code'] + + description = tools['run_code'].tool_def.description + assert description is not None + assert 'async def add(*, a: int, b: int)' in description + # `greet` is exposed natively, so it must NOT appear inside the run_code description + assert 'async def greet' not in description + + async def test_native_tool_call_passes_through(self) -> None: + """Calling a native (non-sandboxed) tool passes through to the wrapped toolset.""" + capability = CodeMode[None](tools=lambda ctx, td: td.name == 'add') + wrapper = capability.get_wrapper_toolset(_build_function_toolset(add, greet)) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + result = await wrapper.call_tool('greet', {'name': 'Alice', 'greeting': 'Hi'}, ctx, tools['greet']) + assert result == 'Hi, Alice!' + + async def test_native_tool_named_run_code_raises_user_error(self) -> None: + """A native tool named `run_code` raises UserError (reserved name).""" + from pydantic_ai.exceptions import UserError + + def run_code() -> str: + """A tool that collides with the reserved name.""" + return 'oops' # pragma: no cover + + capability = CodeMode[None](tools=lambda ctx, td: td.name != 'run_code') + wrapper = capability.get_wrapper_toolset(_build_function_toolset(run_code, add)) + assert isinstance(wrapper, CodeModeToolset) + + with pytest.raises(UserError, match="'run_code' is reserved"): + await wrapper.get_tools(build_run_context(None)) + + async def test_sandboxed_tool_named_run_code_raises_user_error(self) -> None: + """A sandboxed tool named `run_code` raises UserError (conflicts with meta-tool).""" + from pydantic_ai.exceptions import UserError + + def run_code() -> str: + """A tool that collides with the meta-tool name.""" + return 'oops' # pragma: no cover + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(run_code, add)) + assert isinstance(wrapper, CodeModeToolset) + + with pytest.raises(UserError, match='conflicts with the code mode'): + await wrapper.get_tools(build_run_context(None)) + + async def test_filter_excluding_everything_yields_run_code_with_no_functions(self) -> None: + """A filter that rejects every tool produces a `run_code` with no functions block.""" + capability = CodeMode[None](tools=lambda ctx, td: False) + wrapper = capability.get_wrapper_toolset(_build_function_toolset(add, greet)) + assert isinstance(wrapper, CodeModeToolset) + + tools = await wrapper.get_tools(build_run_context(None)) + assert sorted(tools.keys()) == ['add', 'greet', 'run_code'] + + description = tools['run_code'].tool_def.description + assert description is not None + assert 'functions are available inside the sandbox' not in description + + async def test_filter_uses_run_context_for_dynamic_decisions(self) -> None: + """The filter receives the live `RunContext` so it can vary per run/step.""" + seen_steps: list[int] = [] + + def filter_func(ctx: RunContext[None], td: Any) -> bool: + seen_steps.append(ctx.run_step) + return td.name == 'add' + + wrapper = CodeMode[None](tools=filter_func).get_wrapper_toolset(_build_function_toolset(add, greet)) + assert isinstance(wrapper, CodeModeToolset) + await wrapper.get_tools(build_run_context(None, run_step=7)) + assert 7 in seen_steps + + # --------------------------------------------------------------------------- + # TypedDict prelude rendering + # --------------------------------------------------------------------------- + + async def test_typed_dict_arguments_render_as_prelude(self) -> None: + """Tools with structured (TypedDict) parameters render their types in the prelude.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(lookup_person)) + assert isinstance(wrapper, CodeModeToolset) + + description = (await wrapper.get_tools(build_run_context(None)))['run_code'].tool_def.description + assert description is not None + # Type prelude + assert 'class Address(TypedDict):' in description + assert 'street: str' in description + assert 'class Person(TypedDict):' in description + assert 'home: Address' in description + # Function signature references the TypedDict + assert 'async def lookup_person(*, person: Person, count: int = 1) -> str' in description + + async def test_typed_dict_argument_round_trips_through_monty(self) -> None: + """End-to-end with a structured argument: dict literal flows through Monty into the tool. + + The dict literal is constructed incrementally across two REPL calls so + that static type checking (which only runs on the first snippet) doesn't + reject the dict-to-TypedDict coercion that Monty handles at runtime. + """ + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(lookup_person)) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + + # First call sets up variables — type-checked but valid. + await wrapper.call_tool('run_code', {'code': "addr = {'street': '1 Main St', 'city': 'NYC'}"}, ctx, run_code) + # Second call uses them — not type-checked (accumulated REPL state). + code = "p = {'name': 'Alice', 'home': addr}\nprint(await lookup_person(person=p, count=3))" + result = await wrapper.call_tool('run_code', {'code': code}, ctx, run_code) + assert result.return_value == {'output': '3x Alice @ 1 Main St\n'} + + async def test_conflicting_typed_dicts_get_tool_name_prefix(self) -> None: + """Two tools whose `$defs` collide on `Address` get tool-name prefixes in the prelude.""" + user_td = _make_address_tool_def('get_user', 'Get a user.', 'street') + company_td = _make_address_tool_def('get_company', 'Get a company.', 'country') + static = _StaticToolset( + [user_td, company_td], + results={'get_user': 'user-result', 'get_company': 'company-result'}, + ) + + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + description = tools['run_code'].tool_def.description + assert description is not None + # Both conflicting `Address` types get tool-name prefixes. + assert 'class get_user_Address(TypedDict):' in description + assert 'class get_company_Address(TypedDict):' in description + assert 'addr: get_user_Address' in description + assert 'addr: get_company_Address' in description + + # End-to-end through Monty: both tools are callable from inside the sandbox. + result = await wrapper.call_tool( + 'run_code', + { + 'code': ( + "u = await get_user(addr={'street': 'main'}, label='u')\n" + "c = await get_company(addr={'country': 'usa'}, label='c')\n" + 'print(u, c)' + ), + }, + ctx, + tools['run_code'], + ) + assert result.return_value == {'output': 'user-result company-result\n'} + + # --------------------------------------------------------------------------- + # Deferred tools + # --------------------------------------------------------------------------- + + async def test_deferred_loading_tools_promoted_to_native_with_warning(self) -> None: + """Tools with `defer_loading=True` are excluded from sandbox but promoted to native; warning fires once.""" + + def later() -> str: + """A deferred tool.""" + return 'later' # pragma: no cover - deferred tools are promoted to native, not sandboxed + + toolset = FunctionToolset[None](tools=[Tool(add), Tool(later, defer_loading=True)]) + wrapper = CodeMode[None]().get_wrapper_toolset(toolset) + assert isinstance(wrapper, CodeModeToolset) + + ctx = build_run_context(None) + with pytest.warns(UserWarning, match=r"tool 'later' uses deferred loading"): + tools = await wrapper.get_tools(ctx) + + description = tools['run_code'].tool_def.description + assert description is not None + assert 'async def add' in description + assert 'async def later' not in description + # Deferred tool is promoted to native — not lost + assert 'later' in tools + + # Second `get_tools` call must not warn again — the set is preserved across calls + # within the same toolset instance. + import warnings as _warnings + + with _warnings.catch_warnings(): + _warnings.simplefilter('error') + await wrapper.get_tools(ctx) + + async def test_deferred_execution_tools_promoted_to_native_with_warning(self) -> None: + """Tools with `kind='external'` (deferred execution) are excluded from sandbox but promoted to native.""" + td_external = ToolDefinition( + name='approve_action', + description='Needs approval.', + parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'string'}}, 'required': ['x']}, + kind='external', + ) + static = _StaticToolset([_make_address_tool_def('get_user', 'Get a user.', 'street'), td_external]) + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + ctx = build_run_context(None) + with pytest.warns(UserWarning, match=r"tool 'approve_action' requires deferred execution"): + tools = await wrapper.get_tools(ctx) + + description = tools['run_code'].tool_def.description + assert description is not None + assert 'approve_action' not in description + # External tool is promoted to native — not lost + assert 'approve_action' in tools + + # Second call must not warn again. + import warnings as _warnings + + with _warnings.catch_warnings(): + _warnings.simplefilter('error') + await wrapper.get_tools(ctx) + + async def test_tool_without_return_schema_warns(self) -> None: + """A sandboxed tool with no return_schema triggers a one-time warning.""" + td = ToolDefinition( + name='search', + description='Search for things.', + parameters_json_schema={'type': 'object', 'properties': {'q': {'type': 'string'}}, 'required': ['q']}, + # No return_schema — simulates an MCP tool without outputSchema. + ) + static = _StaticToolset([td], results={'search': 'found it'}) + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + ctx = build_run_context(None) + with pytest.warns(UserWarning, match=r"tool 'search' has no return schema"): + tools = await wrapper.get_tools(ctx) + + # Tool is still callable despite the warning. + description = tools['run_code'].tool_def.description + assert description is not None + assert 'async def search' in description + + # Second call must not warn again. + import warnings as _warnings + + with _warnings.catch_warnings(): + _warnings.simplefilter('error') + await wrapper.get_tools(ctx) + + async def test_tool_with_return_schema_does_not_warn(self) -> None: + """A sandboxed tool WITH a return_schema does not trigger the warning.""" + import warnings as _warnings + + td = ToolDefinition( + name='get_user', + description='Get a user.', + parameters_json_schema={'type': 'object', 'properties': {'id': {'type': 'integer'}}, 'required': ['id']}, + return_schema={'type': 'object', 'properties': {'name': {'type': 'string'}}}, + ) + static = _StaticToolset([td], results={'get_user': {'name': 'Alice'}}) + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + with _warnings.catch_warnings(): + _warnings.simplefilter('error') + await wrapper.get_tools(build_run_context(None)) + + # --------------------------------------------------------------------------- + # Agent.run end-to-end (with FunctionModel hand-driving the model output) + # --------------------------------------------------------------------------- + + async def test_code_mode_via_agent_run_executes_run_code_and_returns_result(self) -> None: + """End-to-end through `Agent.run`: a `FunctionModel` issues a `run_code` call, the + sandbox dispatches to a wrapped tool, and the second model turn observes the + tool's return value before producing the final text output. + """ + from pydantic_ai.messages import ( + ModelMessage, + ModelRequest, + ModelResponse, + TextPart, + ToolCallPart, + ToolReturnPart, + ) + from pydantic_ai.models.function import AgentInfo, FunctionModel + + observed_tool_calls: list[str] = [] + observed_tool_returns: list[Any] = [] + seen_tool_definitions: list[list[str]] = [] + + def model_fn(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: + # Snapshot what tool definitions the model is being shown each turn — + # if `CodeMode` is wired correctly the model only ever sees `run_code`. + seen_tool_definitions.append([td.name for td in info.function_tools]) + + # First turn: issue a `run_code` call that calls the wrapped `add` tool + # through the sandbox. + if not observed_tool_calls: + code = 'result = await add(a=4, b=6)\nprint(f"add returned {result}")\nresult' + observed_tool_calls.append(code) + return ModelResponse(parts=[ToolCallPart(tool_name='run_code', args={'code': code})]) + + # Second turn: pull the `run_code` return value out of the most recent + # ModelRequest (which is the one Pydantic AI just appended after dispatch). + last_request = messages[-1] + assert isinstance(last_request, ModelRequest) + run_code_return = next( + p for p in last_request.parts if isinstance(p, ToolReturnPart) and p.tool_name == 'run_code' + ) + observed_tool_returns.append(run_code_return.content) + return ModelResponse(parts=[TextPart(f'sum is {observed_tool_returns[-1]["result"]}')]) + + agent: Agent[None, str] = Agent(FunctionModel(model_fn), capabilities=[CodeMode[None]()]) + + @agent.tool_plain + def add(a: int, b: int) -> int: # pyright: ignore[reportUnusedFunction] + """Add two numbers.""" + return a + b + + result = await agent.run('please add 4 and 6') + + # The model was shown only `run_code` — the wrapped `add` tool is hidden behind it. + assert seen_tool_definitions[0] == ['run_code'] + assert seen_tool_definitions[1] == ['run_code'] + + # The first turn issued exactly the code we expected and the sandbox returned + # both the printed output and the value of the trailing expression. + assert len(observed_tool_calls) == 1 + assert len(observed_tool_returns) == 1 + assert observed_tool_returns[0] == {'output': 'add returned 10\n', 'result': 10} + + # The agent's final output reflects the value flowing through the sandbox. + assert result.output == 'sum is 10' + + # --------------------------------------------------------------------------- + # Capability registration + # --------------------------------------------------------------------------- + + async def test_code_mode_can_be_registered_as_agent_capability(self) -> None: + """`CodeMode` can be passed via `Agent(capabilities=[...])` without raising.""" + Agent(TestModel(), capabilities=[CodeMode[None]()]) + + # --------------------------------------------------------------------------- + # Tool name sanitization + # --------------------------------------------------------------------------- + + @pytest.mark.parametrize( + 'original, expected', + [ + ('get_weather', 'get_weather'), # already valid — no change + ('get-weather', 'get_weather'), # hyphen → underscore + ('api.call', 'api_call'), # dot → underscore + ('api.call-now', 'api_call_now'), # mixed + ('123tool', '_123tool'), # leading digit → prepend underscore + ('a', 'a'), # single char + ('-', '_'), # single invalid char + ('for', 'for_'), # Python keyword → append underscore + ('import', 'import_'), # Python keyword + ], + ) + def test_sanitize_tool_name(self, original: str, expected: str) -> None: + assert _sanitize_tool_name(original) == expected + + async def test_hyphenated_tool_name_is_sanitized_and_callable(self) -> None: + """A tool with hyphens in the name is automatically renamed and callable from the sandbox.""" + td = ToolDefinition( + name='get-weather', + description='Get the weather.', + parameters_json_schema={ + 'type': 'object', + 'properties': {'city': {'type': 'string'}}, + 'required': ['city'], + }, + return_schema={'type': 'string'}, + ) + static = _StaticToolset([td], results={'get-weather': 'sunny'}) + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + description = tools['run_code'].tool_def.description + assert description is not None + # The sanitized name appears in the description, not the original. + assert 'get_weather' in description + assert 'get-weather' not in description + + # End-to-end: the model writes `await get_weather(...)` and the call + # dispatches to the original `get-weather` tool in the wrapped toolset. + result = await wrapper.call_tool( + 'run_code', + {'code': "print(await get_weather(city='NYC'))"}, + ctx, + tools['run_code'], + ) + assert result.return_value == {'output': 'sunny\n'} + + async def test_dotted_tool_name_is_sanitized_and_callable(self) -> None: + """A tool with dots in the name is automatically renamed and callable.""" + td = ToolDefinition( + name='api.lookup', + description='Look up an API.', + parameters_json_schema={ + 'type': 'object', + 'properties': {'key': {'type': 'string'}}, + 'required': ['key'], + }, + return_schema={'type': 'string'}, + ) + static = _StaticToolset([td], results={'api.lookup': 'found'}) + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + result = await wrapper.call_tool( + 'run_code', + {'code': "print(await api_lookup(key='x'))"}, + ctx, + tools['run_code'], + ) + assert result.return_value == {'output': 'found\n'} + + async def test_sanitized_name_collision_warns_and_drops_second(self) -> None: + """When two tool names sanitize to the same identifier, the second is dropped with a warning.""" + td1 = ToolDefinition( + name='get-weather', + description='Get weather (hyphens).', + parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'string'}}, 'required': ['x']}, + return_schema={'type': 'string'}, + ) + td2 = ToolDefinition( + name='get.weather', + description='Get weather (dots).', + parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'string'}}, 'required': ['x']}, + return_schema={'type': 'string'}, + ) + static = _StaticToolset([td1, td2], results={'get-weather': 'rain'}) + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + ctx = build_run_context(None) + with pytest.warns(UserWarning, match=r"tool 'get\.weather'.*collides with 'get-weather'"): + tools = await wrapper.get_tools(ctx) + + description = tools['run_code'].tool_def.description + assert description is not None + # Only the first tool survives. + assert description.count('get_weather') >= 1 + assert 'Get weather (dots)' not in description + + async def test_sanitized_name_collision_with_native_tool(self) -> None: + """A sanitized name that collides with a native (already valid) tool is dropped.""" + td_native = ToolDefinition( + name='get_weather', + description='Native tool.', + parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'string'}}, 'required': ['x']}, + return_schema={'type': 'string'}, + ) + td_hyphen = ToolDefinition( + name='get-weather', + description='Hyphenated tool.', + parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'string'}}, 'required': ['x']}, + return_schema={'type': 'string'}, + ) + static = _StaticToolset([td_native, td_hyphen], results={'get_weather': 'ok'}) + wrapper = CodeMode[None]().get_wrapper_toolset(static) + assert isinstance(wrapper, CodeModeToolset) + + ctx = build_run_context(None) + with pytest.warns(UserWarning, match=r"tool 'get-weather'.*collides with 'get_weather'"): + tools = await wrapper.get_tools(ctx) + + description = tools['run_code'].tool_def.description + assert description is not None + assert 'Native tool' in description + assert 'Hyphenated tool' not in description + + # --------------------------------------------------------------------------- + # Logfire metadata + # --------------------------------------------------------------------------- + + async def test_run_code_tool_has_code_metadata(self) -> None: + """The `run_code` ToolDefinition carries metadata for Logfire code rendering.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + + tools = await wrapper.get_tools(build_run_context(None)) + metadata = tools['run_code'].tool_def.metadata + assert metadata is not None + assert metadata['code_arg_name'] == 'code' + assert metadata['code_arg_language'] == 'python' + + async def test_tool_returning_tool_return_is_unwrapped(self) -> None: + """A wrapped tool that returns a `ToolReturn` has its value unwrapped for the sandbox.""" + from pydantic_ai.messages import ToolReturn as ToolReturnMsg + + def fancy() -> Any: + """Return a ToolReturn with metadata.""" + return ToolReturnMsg(return_value=42, metadata={'source': 'test'}) + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(fancy)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + result = await wrapper.call_tool('run_code', {'code': 'await fancy()'}, ctx, tools['run_code']) + # The sandbox receives the unwrapped value (42), not the ToolReturn wrapper. + # No print output → result returned directly. + assert result.return_value == 42 + + # The nested ToolReturnPart carries the ToolReturn metadata. + returns = result.metadata['tool_returns'] + assert returns['pyd_ai_code_mode__1'].metadata == {'source': 'test'} + + async def test_approval_required_surfaces_as_model_retry(self) -> None: + """Tools that raise ApprovalRequired inside the sandbox surface as ModelRetry.""" + from pydantic_ai.exceptions import ApprovalRequired as _ApprovalRequired + + def needs_approval() -> str: + """A tool that requires approval.""" + raise _ApprovalRequired() + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(needs_approval)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + with pytest.raises(ModelRetry, match='approval and deferral are not supported'): + await wrapper.call_tool('run_code', {'code': 'await needs_approval()'}, ctx, tools['run_code']) + + async def test_model_retry_from_wrapped_tool_surfaces_as_model_retry(self) -> None: + """A wrapped tool that raises ModelRetry gets double-wrapped through Monty but still retries. + + The flow is: ModelRetry → Monty catches as RuntimeError → MontyRuntimeError → ModelRetry. + The original error message is preserved in the display string. + """ + + def flaky() -> str: + """A tool that always retries.""" + raise ModelRetry('try again please') + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(flaky)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + with pytest.raises(ModelRetry, match='try again please'): + await wrapper.call_tool('run_code', {'code': 'await flaky()'}, ctx, tools['run_code']) + + async def test_invalid_tool_args_surface_as_model_retry(self) -> None: + """Wrong argument types passed to a sandboxed tool surface as ModelRetry. + + On a fresh REPL, the static type checker catches this before execution. + """ + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + # Pass a string where int is expected — type checker catches this. + with pytest.raises(ModelRetry, match='error in code'): + await wrapper.call_tool( + 'run_code', + {'code': "await add(a='not_a_number', b=3)"}, + ctx, + tools['run_code'], + ) + + # --------------------------------------------------------------------------- + # Multimodal tool returns + # --------------------------------------------------------------------------- + + async def test_tool_returning_binary_image_is_returned_directly(self) -> None: + """A tool that returns BinaryContent passes through the sandbox and is + returned as native multimodal content (not wrapped in a dict).""" + from pydantic_ai.messages import BinaryContent + + image_bytes = b'\x89PNG\r\n\x1a\n fake image data' + + def gen_image() -> Any: + """Generate an image.""" + return BinaryContent(data=image_bytes, media_type='image/png') + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(gen_image)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + result = await wrapper.call_tool('run_code', {'code': 'await gen_image()'}, ctx, tools['run_code']) + # No print → multimodal content returned directly for native model delivery. + rv = result.return_value + assert isinstance(rv, BinaryContent) + assert rv.data == image_bytes + assert rv.media_type == 'image/png' + + async def test_tool_returning_binary_image_with_print_uses_list_format(self) -> None: + """When print output accompanies a multimodal return, the result is a list + so _split_content can extract the image for native delivery.""" + from pydantic_ai.messages import BinaryContent + + image_bytes = b'\x89PNG fake' + + def gen_image() -> Any: + """Generate an image.""" + return BinaryContent(data=image_bytes, media_type='image/png') + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(gen_image)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + result = await wrapper.call_tool( + 'run_code', + {'code': 'img = await gen_image()\nprint("generated")\nimg'}, + ctx, + tools['run_code'], + ) + # Print + multimodal → list format. + rv = result.return_value + assert isinstance(rv, list) + assert rv[0] == 'generated\n' + assert isinstance(rv[1], BinaryContent) + assert rv[1].data == image_bytes + + async def test_tool_returning_list_with_binary_image_and_print(self) -> None: + """A list result containing multimodal items with print output gets flattened + so _split_content can find each multimodal item at the top level.""" + from pydantic_ai.messages import BinaryContent + + image_bytes = b'\x89PNG list' + + def gen_images() -> Any: + """Generate a list with an image.""" + return [BinaryContent(data=image_bytes, media_type='image/png'), 'caption'] + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(gen_images)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + result = await wrapper.call_tool( + 'run_code', + {'code': 'imgs = await gen_images()\nprint("done")\nimgs'}, + ctx, + tools['run_code'], + ) + # Print + list-with-multimodal → flattened list. + rv = result.return_value + assert isinstance(rv, list) + assert rv[0] == 'done\n' + assert isinstance(rv[1], BinaryContent) + assert rv[1].data == image_bytes + assert rv[2] == 'caption' + + async def test_tool_returning_tool_return_with_binary_content(self) -> None: + """A tool that wraps a BinaryContent in a ToolReturn has the image properly unwrapped + and returned as native multimodal content.""" + from pydantic_ai.messages import BinaryContent + from pydantic_ai.messages import ToolReturn as ToolReturnMsg + + image_bytes = b'\x89PNG wrapped' + + def gen_image() -> Any: + """Generate an image wrapped in ToolReturn.""" + return ToolReturnMsg( + return_value=BinaryContent(data=image_bytes, media_type='image/png'), metadata={'src': 'test'} + ) + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(gen_image)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + result = await wrapper.call_tool('run_code', {'code': 'await gen_image()'}, ctx, tools['run_code']) + rv = result.return_value + assert isinstance(rv, BinaryContent) + assert rv.data == image_bytes + # ToolReturn metadata is preserved on the nested return part. + returns = result.metadata['tool_returns'] + assert returns['pyd_ai_code_mode__1'].metadata == {'src': 'test'} + + # --------------------------------------------------------------------------- + # OTel / Logfire instrumentation + # --------------------------------------------------------------------------- + + @pytest.mark.skipif(not logfire_installed, reason='logfire not installed') + async def test_sandboxed_tool_calls_produce_otel_spans(self, capfire: CaptureLogfire) -> None: + """Sandboxed tool calls dispatched through ToolManager produce OTel execute_tool spans.""" + from pydantic_ai.messages import ( + ModelMessage, + ModelResponse, + TextPart, + ToolCallPart, + ) + from pydantic_ai.models.function import AgentInfo, FunctionModel + from pydantic_ai.models.instrumented import InstrumentationSettings + + call_count = 0 + + def model_fn(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: + nonlocal call_count + call_count += 1 + if call_count == 1: + return ModelResponse(parts=[ToolCallPart(tool_name='run_code', args={'code': 'await add(a=1, b=2)'})]) + return ModelResponse(parts=[TextPart('done')]) + + agent: Agent[None, str] = Agent( + FunctionModel(model_fn), + capabilities=[CodeMode[None]()], + instrument=InstrumentationSettings(include_content=True), + ) + + @agent.tool_plain + def add(a: int, b: int) -> int: # pyright: ignore[reportUnusedFunction] + """Add two numbers.""" + return a + b + + result = await agent.run('test') + assert result.output == 'done' + + spans = capfire.exporter.exported_spans_as_dict() + tool_spans = [s for s in spans if s['attributes'].get('gen_ai.tool.name')] + tool_names = [s['attributes']['gen_ai.tool.name'] for s in tool_spans] + + # The outer `run_code` tool call should produce a span. + assert 'run_code' in tool_names, f'No run_code span found in {tool_names}' + + # The inner `add` tool call (dispatched through ToolManager) should also produce a span. + assert 'add' in tool_names, f'No add span found in {tool_names}' + + # Verify the inner tool span has the expected OTel attributes. + add_span = next(s for s in tool_spans if s['attributes']['gen_ai.tool.name'] == 'add') + assert add_span['attributes']['gen_ai.tool.name'] == 'add' + assert 'gen_ai.tool.call.id' in add_span['attributes'] + + # --------------------------------------------------------------------------- + # Error handling improvements + # --------------------------------------------------------------------------- + + async def test_unknown_function_call_surfaces_as_model_retry(self) -> None: + """Calling an undefined function from sandbox code surfaces as ModelRetry. + + On a fresh REPL, the type checker catches this; on subsequent calls, + it becomes a runtime NameError. + """ + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + + # First call (fresh REPL): type checker catches undefined name. + with pytest.raises(ModelRetry, match='error in code'): + await wrapper.call_tool('run_code', {'code': 'await nonexistent_tool(x=1)'}, ctx, run_code) + + # After a successful call, type checking is skipped — falls to runtime NameError. + await wrapper.call_tool('run_code', {'code': '1 + 1', 'restart': True}, ctx, run_code) + with pytest.raises(ModelRetry, match='Runtime error'): + await wrapper.call_tool('run_code', {'code': 'await nonexistent_tool(x=1)'}, ctx, run_code) + + async def test_positional_args_rejected(self) -> None: + """Calling a tool with positional args surfaces as ModelRetry. + + On a fresh REPL the type checker catches it; on subsequent calls + the runtime positional-args guard catches it. + """ + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + + # Fresh REPL: type checker catches positional args. + with pytest.raises(ModelRetry, match='error in code'): + await wrapper.call_tool('run_code', {'code': 'await add(1, 2)'}, ctx, run_code) + + # After a valid call, type checking is skipped — runtime guard catches it. + await wrapper.call_tool('run_code', {'code': '1 + 1', 'restart': True}, ctx, run_code) + with pytest.raises(ModelRetry, match='does not accept positional arguments'): + await wrapper.call_tool('run_code', {'code': 'await add(1, 2)'}, ctx, run_code) + + # Caught positional args — sandbox code handles the error gracefully. + result = await wrapper.call_tool( + 'run_code', + {'code': 'try:\n await add(1, 2)\nexcept TypeError:\n pass\n"recovered"'}, + ctx, + run_code, + ) + assert result.return_value == 'recovered' + + async def test_print_output_preserved_in_runtime_error(self) -> None: + """When sandbox code prints before crashing, the print output is included + in the ModelRetry error message so the model can use it for debugging.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + + with pytest.raises(ModelRetry, match=r'Runtime error') as exc_info: + await wrapper.call_tool( + 'run_code', + {'code': 'print("debug info")\n1 / 0'}, + ctx, + tools['run_code'], + ) + msg = str(exc_info.value) + assert 'debug info' in msg + assert '[stdout before error]' in msg + + # --------------------------------------------------------------------------- + # Sequential tool resolution + # --------------------------------------------------------------------------- + + async def test_sequential_tool_rendered_as_sync_and_resolved_inline(self) -> None: + """A tool with `sequential=True` is rendered as `def` (sync) and + resolved inline at FunctionSnapshot via `resume(return_value=...)`.""" + from dataclasses import replace as dc_replace + + class _SeqToolset(AbstractToolset[None]): + """Marks add as sequential; greet stays parallel.""" + + def __init__(self) -> None: + self._inner = _build_function_toolset(add, greet) + + @property + def id(self) -> str | None: + return None # pragma: no cover + + async def get_tools(self, ctx: RunContext[None]) -> dict[str, ToolsetTool[None]]: + tools = await self._inner.get_tools(ctx) + return { + n: dc_replace(t, tool_def=dc_replace(t.tool_def, sequential=True)) if n == 'add' else t + for n, t in tools.items() + } + + async def call_tool( + self, name: str, tool_args: dict[str, Any], ctx: RunContext[None], tool: ToolsetTool[None] + ) -> Any: + return await self._inner.call_tool(name, tool_args, ctx, tool) + + seq_wrapper = CodeModeToolset[None](wrapped=_SeqToolset(), tool_selector='all') + ctx = await build_ctx(None, seq_wrapper) + tools = await seq_wrapper.get_tools(ctx) + run_code = tools['run_code'] + + # Sequential tool rendered as `def`, parallel tool as `async def`. + desc = run_code.tool_def.description or '' + assert 'def add(' in desc + assert 'async def add(' not in desc + assert 'async def greet(' in desc + + # Sequential tool called without `await`, parallel with `await`. + result = await seq_wrapper.call_tool( + 'run_code', + { + 'code': 'result_add = add(a=1, b=2)\nresult_greet = await greet(name="World")\n[result_add, result_greet]' + }, + ctx, + run_code, + ) + assert result.return_value == [3, 'Hello, World!'] + + # Metadata records both sequential and parallel tool calls. + assert result.metadata['code_mode'] is True + calls = result.metadata['tool_calls'] + returns = result.metadata['tool_returns'] + assert len(calls) == 2 + assert len(returns) == 2 + call_names = {c.tool_name for c in calls.values()} + assert call_names == {'add', 'greet'} + for tc_id, call in calls.items(): + assert tc_id in returns + assert returns[tc_id].tool_name == call.tool_name + + async def test_sequential_tool_barrier_awaits_pending_parallel_tasks(self) -> None: + """When a sequential tool is called while parallel tasks are pending, + the pending tasks are awaited first (barrier) before dispatching.""" + from dataclasses import replace as dc_replace + + class _SeqToolset(AbstractToolset[None]): + def __init__(self) -> None: + self._inner = _build_function_toolset(add, greet) + + @property + def id(self) -> str | None: + return None # pragma: no cover + + async def get_tools(self, ctx: RunContext[None]) -> dict[str, ToolsetTool[None]]: + tools = await self._inner.get_tools(ctx) + return { + n: dc_replace(t, tool_def=dc_replace(t.tool_def, sequential=True)) if n == 'add' else t + for n, t in tools.items() + } + + async def call_tool( + self, name: str, tool_args: dict[str, Any], ctx: RunContext[None], tool: ToolsetTool[None] + ) -> Any: + return await self._inner.call_tool(name, tool_args, ctx, tool) + + seq_wrapper = CodeModeToolset[None](wrapped=_SeqToolset(), tool_selector='all') + ctx = await build_ctx(None, seq_wrapper) + tools = await seq_wrapper.get_tools(ctx) + + # Start a parallel call (greet, async def), then call a sequential tool (add, def). + # The barrier should await greet before dispatching add. + result = await seq_wrapper.call_tool( + 'run_code', + { + 'code': ( + 'future_greet = greet(name="World")\n' + 'result_add = add(a=1, b=2)\n' + 'result_greet = await future_greet\n' + '[result_add, result_greet]' + ) + }, + ctx, + tools['run_code'], + ) + assert result.return_value == [3, 'Hello, World!'] + + # Both calls recorded in metadata — greet resolved at barrier, add resolved inline. + assert result.metadata['code_mode'] is True + calls = result.metadata['tool_calls'] + returns = result.metadata['tool_returns'] + assert len(calls) == 2 + assert len(returns) == 2 + # greet was dispatched first (parallel), add second (sequential barrier). + call_list = list(calls.values()) + assert call_list[0].tool_name == 'greet' + assert call_list[1].tool_name == 'add' + for tc_id in calls: + assert returns[tc_id].content in (3, 'Hello, World!') + + async def test_sequential_tool_error_surfaces_as_model_retry(self) -> None: + """An error from a sequential tool (resolved inline) surfaces as ModelRetry.""" + from dataclasses import replace as dc_replace + + class _SeqToolset(AbstractToolset[None]): + def __init__(self) -> None: + self._inner = _build_function_toolset(add) + + @property + def id(self) -> str | None: + return None # pragma: no cover + + async def get_tools(self, ctx: RunContext[None]) -> dict[str, ToolsetTool[None]]: + tools = await self._inner.get_tools(ctx) + return {n: dc_replace(t, tool_def=dc_replace(t.tool_def, sequential=True)) for n, t in tools.items()} + + async def call_tool( + self, name: str, tool_args: dict[str, Any], ctx: RunContext[None], tool: ToolsetTool[None] + ) -> Any: + return await self._inner.call_tool(name, tool_args, ctx, tool) + + seq_wrapper = CodeModeToolset[None](wrapped=_SeqToolset(), tool_selector='all') + ctx = await build_ctx(None, seq_wrapper) + tools = await seq_wrapper.get_tools(ctx) + run_code = tools['run_code'] + # Make a successful call so the REPL is no longer fresh (type checking skipped). + await seq_wrapper.call_tool('run_code', {'code': 'add(a=1, b=2)'}, ctx, run_code) + # Now bad args go through the runtime path in sequential resolution. + with pytest.raises(ModelRetry, match='Runtime error'): + await seq_wrapper.call_tool('run_code', {'code': "add(a='bad', b=3)"}, ctx, run_code) + + async def test_global_sequential_mode_forces_sequential_resolution(self) -> None: + """When the parallel execution mode is `sequential`, tool calls inside the + sandbox are resolved sequentially via FutureSnapshot. Signatures stay `async def`.""" + from pydantic_ai.tool_manager import ToolManager + + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + + with ToolManager.parallel_execution_mode('sequential'): + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + + # All tools are still rendered as `async def` (global mode doesn't affect rendering). + desc = run_code.tool_def.description + assert desc is not None + assert 'async def add(' in desc + + result = await wrapper.call_tool( + 'run_code', + {'code': 'await add(a=10, b=20)'}, + ctx, + run_code, + ) + assert result.return_value == 30 + + async def test_global_sequential_overrides_per_tool_sequential(self) -> None: + """When global sequential mode is active AND a tool has `sequential=True`, + the tool is deferred (not resolved inline) and handled via FutureSnapshot.""" + from dataclasses import replace as dc_replace + + from pydantic_ai.tool_manager import ToolManager + + class _SeqToolset(AbstractToolset[None]): + def __init__(self) -> None: + self._inner = _build_function_toolset(add) + + @property + def id(self) -> str | None: + return None # pragma: no cover + + async def get_tools(self, ctx: RunContext[None]) -> dict[str, ToolsetTool[None]]: + tools = await self._inner.get_tools(ctx) + return {n: dc_replace(t, tool_def=dc_replace(t.tool_def, sequential=True)) for n, t in tools.items()} + + async def call_tool( + self, name: str, tool_args: dict[str, Any], ctx: RunContext[None], tool: ToolsetTool[None] + ) -> Any: + return await self._inner.call_tool(name, tool_args, ctx, tool) + + seq_wrapper = CodeModeToolset[None](wrapped=_SeqToolset(), tool_selector='all') + ctx = await build_ctx(None, seq_wrapper) + + with ToolManager.parallel_execution_mode('sequential'): + tools = await seq_wrapper.get_tools(ctx) + run_code = tools['run_code'] + + # Per-tool sequential renders as `def`, but global mode uses deferred path. + desc = run_code.tool_def.description or '' + assert 'def add(' in desc + assert 'async def add(' not in desc + + # The tool still works — global sequential resolves at FutureSnapshot. + result = await seq_wrapper.call_tool('run_code', {'code': 'add(a=5, b=7)'}, ctx, run_code) + assert result.return_value == 12 + + async def test_restart_with_invalid_code_clears_repl_for_retry(self) -> None: + """When `restart=True` and type checking fails, the REPL is cleared so + the next retry still gets type-checked on a fresh REPL.""" + wrapper = CodeMode[None]().get_wrapper_toolset(_build_function_toolset(add)) + assert isinstance(wrapper, CodeModeToolset) + ctx = await build_ctx(None, wrapper) + tools = await wrapper.get_tools(ctx) + run_code = tools['run_code'] + + # First call succeeds — REPL has state. + await wrapper.call_tool('run_code', {'code': 'x = await add(a=1, b=2)'}, ctx, run_code) + + # Restart with bad code — type checking catches it. + with pytest.raises(ModelRetry, match='Type error'): + await wrapper.call_tool('run_code', {'code': "await add(a='bad', b=3)", 'restart': True}, ctx, run_code) + + # Retry without restart — should still be type-checked (REPL was cleared). + with pytest.raises(ModelRetry, match='Type error'): + await wrapper.call_tool('run_code', {'code': "await add(a='bad', b=3)"}, ctx, run_code) + + # --------------------------------------------------------------------------- + # Internal helpers + # --------------------------------------------------------------------------- + + def test_print_capture_concatenates_chunks_in_order(self) -> None: + """`_PrintCapture` accumulates print-callback chunks and joins them on read. + + Lives in the production module rather than as a closure inside `call_tool` so + coverage.py sees it execute even when Monty's Rust-side worker thread bypasses + the per-thread tracer hooks. This unit test exercises it directly. + """ + capture = _PrintCapture() + assert capture.joined == '' + capture('stdout', 'hello') + capture('stdout', ' ') + capture('stdout', 'world\n') + assert capture.joined == 'hello world\n' + + +class TestToolSearchIntegration: + """Tests for CodeMode + ToolSearch (search_tools) interaction.""" + + async def test_search_tool_stays_native(self) -> None: + """search_tools is kept as a native tool even with tools='all'.""" + from pydantic_ai.toolsets._tool_search import _SEARCH_TOOLS_NAME + from pydantic_ai.toolsets.combined import CombinedToolset + + search_toolset = _StaticToolset([_search_tool_def()]) + func_toolset = _build_function_toolset(add) + combined = CombinedToolset([search_toolset, func_toolset]) + code_mode = CodeModeToolset(wrapped=combined, tool_selector='all') + ctx = build_run_context(None) + tools = await code_mode.get_tools(ctx) + + # search_tools should be native (not sandboxed inside run_code) + assert _SEARCH_TOOLS_NAME in tools + assert tools[_SEARCH_TOOLS_NAME].tool_def.name == _SEARCH_TOOLS_NAME + # run_code should also be present with the sandboxed 'add' function + assert 'run_code' in tools + # add should be sandboxed (not a separate native tool) + assert 'add' not in tools + + async def test_search_tools_description_appended(self) -> None: + """search_tools description gets a modifier appended about run_code functions.""" + from pydantic_ai.toolsets._tool_search import _SEARCH_TOOLS_NAME + + original_desc = 'There are additional tools. Search here.' + toolset = _StaticToolset([_search_tool_def(description=original_desc)]) + code_mode = CodeModeToolset(wrapped=toolset, tool_selector='all') + ctx = build_run_context(None) + tools = await code_mode.get_tools(ctx) + + modified_desc = tools[_SEARCH_TOOLS_NAME].tool_def.description + assert modified_desc is not None + assert modified_desc.startswith(original_desc) + assert modified_desc.endswith(_SEARCH_TOOLS_MODIFIER) + + async def test_run_code_description_includes_search_note(self) -> None: + """run_code description includes tool search addendum when search_tools present.""" + toolset = _StaticToolset([_search_tool_def()]) + code_mode = CodeModeToolset(wrapped=toolset, tool_selector='all') + ctx = build_run_context(None) + tools = await code_mode.get_tools(ctx) + + run_code_desc = tools['run_code'].tool_def.description + assert run_code_desc is not None + assert _TOOL_SEARCH_ADDENDUM.strip() in run_code_desc + + async def test_run_code_description_no_search_note_without_search_tools(self) -> None: + """run_code description does NOT include search addendum when no search_tools.""" + toolset = _build_function_toolset(add) + code_mode = CodeModeToolset(wrapped=toolset, tool_selector='all') + ctx = build_run_context(None) + tools = await code_mode.get_tools(ctx) + + run_code_desc = tools['run_code'].tool_def.description + assert run_code_desc is not None + assert 'search_tools' not in run_code_desc + + def test_code_mode_ordering(self) -> None: + """CodeMode declares ordering: outermost position, wraps ToolSearch.""" + from pydantic_ai.capabilities._tool_search import ToolSearch + + ordering = CodeMode().get_ordering() + assert ordering is not None + assert ordering.position == 'outermost' + assert ToolSearch in ordering.wraps + + +def _search_tool_def(description: str = 'Search for tools.') -> ToolDefinition: + """Create a ToolDefinition mimicking the search_tools tool from ToolSearchToolset.""" + from pydantic_ai.toolsets._tool_search import _SEARCH_TOOLS_NAME + + return ToolDefinition( + name=_SEARCH_TOOLS_NAME, + description=description, + parameters_json_schema={'type': 'object', 'properties': {'keywords': {'type': 'string'}}}, + ) diff --git a/tests/_code_mode/test_dbos.py b/tests/_code_mode/test_dbos.py new file mode 100644 index 0000000..9502150 --- /dev/null +++ b/tests/_code_mode/test_dbos.py @@ -0,0 +1,149 @@ +"""DBOS integration tests for CodeMode. + +Verifies that the snapshot-based execution loop works inside a DBOS +durable workflow. DBOS uses SQLite locally — no external services needed. + +DBOS defaults to `parallel_ordered_events` execution mode, which triggers +the sequential FutureSnapshot resolution path in the execution loop. +""" + +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +import pytest + +try: + from dbos import DBOS, DBOSConfig + from pydantic_ai.durable_exec.dbos import DBOSAgent +except ImportError: # pragma: lax no cover + pytest.skip('dbos not installed', allow_module_level=True) + +from pydantic_ai import Agent +from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, ToolCallPart, ToolReturnPart +from pydantic_ai.models.function import AgentInfo, FunctionModel +from pydantic_ai.toolsets.function import FunctionToolset + +from pydantic_harness import CodeMode + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope='module') +def dbos_instance(tmp_path_factory: pytest.TempPathFactory) -> Generator[DBOS, Any, None]: + dbos_sqlite_file = tmp_path_factory.mktemp('dbos') / 'dbostest.sqlite' + dbos_config: DBOSConfig = { + 'name': 'pydantic_harness_dbos_tests', + 'system_database_url': f'sqlite:///{dbos_sqlite_file}', + 'run_admin_server': False, + 'enable_otlp': False, + } + dbos = DBOS(config=dbos_config) + DBOS.launch() + try: + yield dbos + finally: + DBOS.destroy() + + +# --------------------------------------------------------------------------- +# Tools and agents (module-level) +# --------------------------------------------------------------------------- + + +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + +def _code_mode_model(messages: list[ModelRequest | ModelResponse], info: AgentInfo) -> ModelResponse: + for msg in messages: + if isinstance(msg, ModelResponse): + continue + for part in msg.parts: + if isinstance(part, ToolReturnPart) and part.tool_name == 'run_code': + return ModelResponse(parts=[TextPart(content=f'done: {part.content}')]) + + return ModelResponse( + parts=[ + ToolCallPart( + tool_name='run_code', + args={'code': 'result = await add(a=3, b=4)\nresult'}, + tool_call_id='test_tc_1', + ) + ] + ) + + +code_mode_agent = Agent( + FunctionModel(_code_mode_model), + name='code_mode_dbos_agent', + toolsets=[FunctionToolset(tools=[add], id='math')], + capabilities=[CodeMode()], +) + +dbos_code_mode_agent = DBOSAgent(code_mode_agent) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +def test_code_mode_runs_in_dbos_workflow(dbos_instance: DBOS) -> None: + """CodeMode's snapshot-based execution loop works inside a DBOS durable + workflow. DBOS defaults to `parallel_ordered_events` mode, which triggers + the sequential FutureSnapshot resolution path.""" + result = dbos_code_mode_agent.run_sync('Calculate 3 + 4') + assert result.output == 'done: 7' + + messages = result.all_messages() + assert len(messages) == 4 + + # 1. User prompt + assert isinstance(messages[0], ModelRequest) + user_part = messages[0].parts[0] + assert user_part.part_kind == 'user-prompt' + assert user_part.content == 'Calculate 3 + 4' # pyright: ignore[reportUnknownMemberType] + + # 2. Model response — run_code tool call + assert isinstance(messages[1], ModelResponse) + tc = messages[1].parts[0] + assert isinstance(tc, ToolCallPart) + assert tc.tool_name == 'run_code' + assert tc.args == {'code': 'result = await add(a=3, b=4)\nresult'} + assert tc.tool_call_id == 'test_tc_1' + + # 3. Tool return with nested tool call metadata + assert isinstance(messages[2], ModelRequest) + tr = messages[2].parts[0] + assert isinstance(tr, ToolReturnPart) + assert tr.tool_name == 'run_code' + assert tr.content == 7 + assert tr.tool_call_id == 'test_tc_1' + + # Verify nested tool call/return metadata + assert tr.metadata is not None + assert tr.metadata['code_mode'] is True + nested_calls: dict[str, ToolCallPart] = tr.metadata['tool_calls'] + nested_returns: dict[str, ToolReturnPart] = tr.metadata['tool_returns'] + assert len(nested_calls) == 1 + assert len(nested_returns) == 1 + + nested_call = next(iter(nested_calls.values())) + assert nested_call.tool_name == 'add' + assert nested_call.args == {'a': 3, 'b': 4} + + nested_return = next(iter(nested_returns.values())) + assert nested_return.tool_name == 'add' + assert nested_return.content == 7 + assert nested_return.tool_call_id == nested_call.tool_call_id + + # 4. Final text response + assert isinstance(messages[3], ModelResponse) + final = messages[3].parts[0] + assert isinstance(final, TextPart) + assert final.content == 'done: 7' diff --git a/tests/_code_mode/test_temporal.py b/tests/_code_mode/test_temporal.py new file mode 100644 index 0000000..91aaf31 --- /dev/null +++ b/tests/_code_mode/test_temporal.py @@ -0,0 +1,208 @@ +"""Temporal integration tests for CodeMode. + +Verifies that the snapshot-based execution loop (`feed_start`/`resume`) +works inside a Temporal workflow sandbox, which forbids threads and +`call_soon_threadsafe`. + +These tests start a local Temporal dev server via +`WorkflowEnvironment.start_local()` — the Temporal SDK downloads and +runs `temporalite` automatically. +""" + +from __future__ import annotations + +import json +from collections.abc import AsyncIterator +from datetime import timedelta +from typing import Any + +import pytest + +try: + from pydantic_ai.durable_exec.temporal import ( + AgentPlugin, + PydanticAIPlugin, + TemporalAgent, + ) + from temporalio import workflow + from temporalio.client import Client + from temporalio.common import RetryPolicy + from temporalio.testing import WorkflowEnvironment + from temporalio.worker import Worker + from temporalio.workflow import ActivityConfig +except ImportError: # pragma: lax no cover + pytest.skip('temporalio not installed', allow_module_level=True) + +from pydantic_ai import Agent +from pydantic_ai.messages import ModelRequest, ModelResponse, TextPart, ToolCallPart, ToolReturnPart +from pydantic_ai.models.function import AgentInfo, FunctionModel +from pydantic_ai.toolsets.function import FunctionToolset + +from pydantic_harness import CodeMode + +pytestmark = pytest.mark.anyio + +TEMPORAL_PORT = 7244 # avoid conflict with other test suites +TASK_QUEUE = 'pydantic-harness-code-mode-queue' +BASE_ACTIVITY_CONFIG = ActivityConfig( + start_to_close_timeout=timedelta(seconds=60), + retry_policy=RetryPolicy(maximum_attempts=1), +) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope='module') +async def temporal_env() -> AsyncIterator[WorkflowEnvironment]: + async with await WorkflowEnvironment.start_local( # pyright: ignore[reportUnknownMemberType] + port=TEMPORAL_PORT, + dev_server_extra_args=[ + '--dynamic-config-value', + 'frontend.enableServerVersionCheck=false', + ], + ) as env: + yield env + + +@pytest.fixture +async def client(temporal_env: WorkflowEnvironment) -> Client: + return await Client.connect( + f'localhost:{TEMPORAL_PORT}', + plugins=[PydanticAIPlugin()], + ) + + +# --------------------------------------------------------------------------- +# Tools and agents (module-level — Temporal requirement) +# --------------------------------------------------------------------------- + + +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + +# FunctionModel that emits a run_code tool call for the given code snippet. +def _code_mode_model(messages: list[ModelRequest | ModelResponse], info: AgentInfo) -> ModelResponse: + """Model that generates a run_code call on the first request, then returns the result as text.""" + # Check if we already got a tool result back. + for msg in messages: + if isinstance(msg, ModelResponse): + continue + for part in msg.parts: + if isinstance(part, ToolReturnPart) and part.tool_name == 'run_code': + return ModelResponse(parts=[TextPart(content=f'done: {part.content}')]) + + # First call — emit run_code. + return ModelResponse( + parts=[ + ToolCallPart( + tool_name='run_code', + args={'code': 'result = await add(a=3, b=4)\nresult'}, + tool_call_id='test_tc_1', + ) + ] + ) + + +code_mode_agent = Agent( + FunctionModel(_code_mode_model), + name='code_mode_temporal_agent', + toolsets=[FunctionToolset(tools=[add], id='math')], + capabilities=[CodeMode()], +) + +temporal_code_mode_agent = TemporalAgent( + code_mode_agent, + activity_config=BASE_ACTIVITY_CONFIG, +) + + +@workflow.defn +class CodeModeWorkflow: + @workflow.run + async def run(self, prompt: str) -> dict[str, Any]: + result = await temporal_code_mode_agent.run(prompt) + return { + 'output': str(result.output), + 'messages': result.all_messages_json().decode(), + } + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +async def test_code_mode_runs_in_temporal_workflow(client: Client) -> None: + """CodeMode's snapshot-based execution loop works inside a Temporal workflow. + + This is the core regression test for the `call_soon_threadsafe` issue: + the old `feed_run_async` approach hung because Temporal's sandboxed + event loop doesn't implement `call_soon_threadsafe`. The snapshot + approach (`feed_start`/`resume`) avoids threads entirely. + """ + async with Worker( + client, + task_queue=TASK_QUEUE, + workflows=[CodeModeWorkflow], + plugins=[AgentPlugin(temporal_code_mode_agent)], + ): + result = await client.execute_workflow( + CodeModeWorkflow.run, + args=['Calculate 3 + 4'], + id='test_code_mode_temporal_1', + task_queue=TASK_QUEUE, + ) + + assert result['output'] == 'done: 7' + + messages = json.loads(result['messages']) + assert len(messages) == 4 + + # 1. User prompt + assert messages[0]['kind'] == 'request' + assert messages[0]['parts'][0]['part_kind'] == 'user-prompt' + assert messages[0]['parts'][0]['content'] == 'Calculate 3 + 4' + + # 2. Model response — run_code tool call + assert messages[1]['kind'] == 'response' + tc = messages[1]['parts'][0] + assert tc['part_kind'] == 'tool-call' + assert tc['tool_name'] == 'run_code' + assert tc['args'] == {'code': 'result = await add(a=3, b=4)\nresult'} + assert tc['tool_call_id'] == 'test_tc_1' + + # 3. Tool return with nested tool call metadata + assert messages[2]['kind'] == 'request' + tr = messages[2]['parts'][0] + assert tr['part_kind'] == 'tool-return' + assert tr['tool_name'] == 'run_code' + assert tr['content'] == 7 + assert tr['tool_call_id'] == 'test_tc_1' + + # Verify nested tool call/return metadata + metadata = tr['metadata'] + assert metadata is not None + assert metadata['code_mode'] is True + nested_calls = metadata['tool_calls'] + nested_returns = metadata['tool_returns'] + assert len(nested_calls) == 1 + assert len(nested_returns) == 1 + + nested_call = next(iter(nested_calls.values())) + assert nested_call['tool_name'] == 'add' + assert nested_call['args'] == {'a': 3, 'b': 4} + + nested_return = next(iter(nested_returns.values())) + assert nested_return['tool_name'] == 'add' + assert nested_return['content'] == 7 + assert nested_return['tool_call_id'] == nested_call['tool_call_id'] + + # 4. Final text response + assert messages[3]['kind'] == 'response' + assert messages[3]['parts'][0]['part_kind'] == 'text' + assert messages[3]['parts'][0]['content'] == 'done: 7' diff --git a/uv.lock b/uv.lock index 0730281..dfaa053 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -76,6 +85,123 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -198,6 +324,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] +[[package]] +name = "dbos" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psycopg", extra = ["binary"] }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "typer-slim" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/5c/994eedfafd2ce2e1190dad78ef39742f8bae53fc3cc3ed44515398ae0963/dbos-2.18.0.tar.gz", hash = "sha256:ed44aca66918c83730200c8c11ebc59f8dbb132362da25c792d6207ed1b73f5e", size = 441502, upload-time = "2026-04-08T16:40:14.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/e3/dce4b64efd82b0a2d9e638e1a98f64ff937cac94ad77550300eddb1fe547/dbos-2.18.0-py3-none-any.whl", hash = "sha256:18358056db9d831b7a1025cdf160981b3b13cd6462a4130033bede1ca6c15512", size = 178320, upload-time = "2026-04-08T16:40:12.371Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -210,6 +353,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + [[package]] name = "genai-prices" version = "0.0.56" @@ -223,6 +375,72 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/f6/8ef7e4c286deb2709d11ca96a5237caae3ef4876ab3c48095856cfd2df30/genai_prices-0.0.56-py3-none-any.whl", hash = "sha256:dbe86be8f3f556bed1b72209ed36851fec8b01793b3b220f42921a4e7da945f6", size = 68966, upload-time = "2026-03-20T20:33:02.555Z" }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +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" } +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" }, +] + +[[package]] +name = "greenlet" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/bc/e30e1e3d5e8860b0e0ce4d2b16b2681b77fd13542fc0d72f7e3c22d16eff/greenlet-3.4.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d18eae9a7fb0f499efcd146b8c9750a2e1f6e0e93b5a382b3481875354a430e6", size = 284315, upload-time = "2026-04-08T17:02:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cc/e023ae1967d2a26737387cac083e99e47f65f58868bd155c4c80c01ec4e0/greenlet-3.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636d2f95c309e35f650e421c23297d5011716be15d966e6328b367c9fc513a82", size = 601916, upload-time = "2026-04-08T16:24:35.533Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/5be1677954b6d8810b33abe94e3eb88726311c58fa777dc97e390f7caf5a/greenlet-3.4.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:234582c20af9742583c3b2ddfbdbb58a756cfff803763ffaae1ac7990a9fac31", size = 616399, upload-time = "2026-04-08T16:30:54.536Z" }, + { url = "https://files.pythonhosted.org/packages/74/bf/2d58d5ea515704f83e34699128c9072a34bea27d2b6a556e102105fe62a5/greenlet-3.4.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:523677e69cd4711b5a014e37bc1fb3a29947c3e3a5bb6a527e1cc50312e5a398", size = 611978, upload-time = "2026-04-08T15:56:31.335Z" }, + { url = "https://files.pythonhosted.org/packages/bd/69/6525049b6c179d8a923256304d8387b8bdd4acab1acf0407852463c6d514/greenlet-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b45e45fe47a19051a396abb22e19e7836a59ee6c5a90f3be427343c37908d65b", size = 1571957, upload-time = "2026-04-08T16:26:17.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/bbfb798b05fec736a0d24dc23e81b45bcee87f45a83cfb39db031853bddc/greenlet-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5434271357be07f3ad0936c312645853b7e689e679e29310e2de09a9ea6c3adf", size = 1637223, upload-time = "2026-04-08T15:57:27.556Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7d/981fe0e7c07bd9d5e7eb18decb8590a11e3955878291f7a7de2e9c668eb7/greenlet-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a19093fbad824ed7c0f355b5ff4214bffda5f1a7f35f29b31fcaa240cc0135ab", size = 237902, upload-time = "2026-04-08T17:03:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c6/dba32cab7e3a625b011aa5647486e2d28423a48845a2998c126dd69c85e1/greenlet-3.4.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:805bebb4945094acbab757d34d6e1098be6de8966009ab9ca54f06ff492def58", size = 285504, upload-time = "2026-04-08T15:52:14.071Z" }, + { url = "https://files.pythonhosted.org/packages/54/f4/7cb5c2b1feb9a1f50e038be79980dfa969aa91979e5e3a18fdbcfad2c517/greenlet-3.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:439fc2f12b9b512d9dfa681c5afe5f6b3232c708d13e6f02c845e0d9f4c2d8c6", size = 605476, upload-time = "2026-04-08T16:24:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/af/b66ab0b2f9a4c5a867c136bf66d9599f34f21a1bcca26a2884a29c450bd9/greenlet-3.4.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70ed1cb0295bee1df57b63bf7f46b4e56a5c93709eea769c1fec1bb23a95875", size = 618336, upload-time = "2026-04-08T16:30:56.59Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5c/8c5633ece6ba611d64bf2770219a98dd439921d6424e4e8cf16b0ac74ea5/greenlet-3.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c660bce1940a1acae5f51f0a064f1bc785d07ea16efcb4bc708090afc4d69e83", size = 613515, upload-time = "2026-04-08T15:56:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/a9/df/950d15bca0d90a0e7395eb777903060504cdb509b7b705631e8fb69ff415/greenlet-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee407d4d1ca9dc632265aee1c8732c4a2d60adff848057cdebfe5fe94eb2c8a2", size = 1574623, upload-time = "2026-04-08T16:26:18.596Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e7/0839afab829fcb7333c9ff6d80c040949510055d2d4d63251f0d1c7c804e/greenlet-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:956215d5e355fffa7c021d168728321fd4d31fd730ac609b1653b450f6a4bc71", size = 1639579, upload-time = "2026-04-08T15:57:29.231Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2b/b4482401e9bcaf9f5c97f67ead38db89c19520ff6d0d6699979c6efcc200/greenlet-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cb614ace7c27571270354e9c9f696554d073f8aa9319079dcba466bbdead711", size = 238233, upload-time = "2026-04-08T17:02:54.286Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4d/d8123a4e0bcd583d5cfc8ddae0bbe29c67aab96711be331a7cc935a35966/greenlet-3.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:04403ac74fe295a361f650818de93be11b5038a78f49ccfb64d3b1be8fbf1267", size = 235045, upload-time = "2026-04-08T17:04:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, + { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, + { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, + { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, + { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" }, + { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, + { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, + { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, + { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932, upload-time = "2026-04-08T17:04:33.551Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, + { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, + { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, + { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" }, +] + [[package]] name = "griffelib" version = "2.0.0" @@ -299,6 +517,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "logfire" +version = "4.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/fc/21f923243d8c3ca2ebfa97de46970ced734e66ac634c1c35b6abb41300f1/logfire-4.31.0.tar.gz", hash = "sha256:361bfda17c9d70ada5d220211033bae06b871ddac9d5b06978bc0ceca6b8e658", size = 1080609, upload-time = "2026-03-27T19:00:46.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/1a/8c860e35bf847ac0d647d94bad89dccbb66cbcafdd61d8334f8cc7cfdd58/logfire-4.31.0-py3-none-any.whl", hash = "sha256:49fad38b5e6f199a98e9c8814e860c8a42595bb81479b52a20413e53ee475b72", size = 308896, upload-time = "2026-03-27T19:00:43.107Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "opentelemetry-instrumentation-httpx" }, +] + [[package]] name = "logfire-api" version = "4.29.0" @@ -308,6 +550,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/cc/62df4abc3e4650c25b81a8e39a1d498d3246c43f3aa4bfab7a73689317b4/logfire_api-4.29.0-py3-none-any.whl", hash = "sha256:48a1361b818357f5a37c71f9683f97e626e5df6c17f35212bfc1f19dddc6771c", size = 121457, upload-time = "2026-03-13T15:30:22.652Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +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" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "nexus-rpc" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/d5/cd1ffb202b76ebc1b33c1332a3416e55a39929006982adc2b1eb069aaa9b/nexus_rpc-1.4.0.tar.gz", hash = "sha256:3b8b373d4865671789cc43623e3dc0bcbf192562e40e13727e17f1c149050fba", size = 82367, upload-time = "2026-02-25T22:01:34.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/52/6327a5f4fda01207205038a106a99848a41c83e933cd23ea2cab3d2ebc6c/nexus_rpc-1.4.0-py3-none-any.whl", hash = "sha256:14c953d3519113f8ccec533a9efdb6b10c28afef75d11cdd6d422640c40b3a49", size = 29645, upload-time = "2026-02-25T22:01:33.122Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -319,15 +594,124 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.40.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, ] [[package]] @@ -360,6 +744,101 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/d8/a763308a41e2ecfb6256ba0877d340c2f2b124c8b2746401863d96fa2c7a/psycopg_binary-3.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3385b58b2fe408a13d084c14b8dcf468cd36cbbe774408250facc128f9fa75c", size = 4609758, upload-time = "2026-02-18T16:46:33.132Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a9/f8a683e85400c1208685e7c895abc049dc13aa0b6ea989e6adf0a3681fe0/psycopg_binary-3.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1bef235a50a80f6aba05147002bc354559657cb6386dbd04d8e1c97d1d7cbe84", size = 4676740, upload-time = "2026-02-18T16:46:42.904Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7d/03512c4aaac8a58fc3b1221f38293aa517a1950d10ef8646c72c49addc7d/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:97c839717bf8c8df3f6d983a20949c4fb22e2a34ee172e3e427ede363feda27b", size = 5496335, upload-time = "2026-02-18T16:46:51.517Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bc/23319b4b1c2c0b810d225e1b6f16efbb16150074fc0ea96bfcabdf59ee09/psycopg_binary-3.3.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:48e500cf1c0984dacf1f28ea482c3cdbb4c2288d51c336c04bc64198ab21fc51", size = 5172032, upload-time = "2026-02-18T16:47:00.878Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/6d61dc0a56654c558a37b2d9b2094e470aa12621305cc7935fd769122e32/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb36a08859b9432d94ea6b26ec41a2f98f83f14868c91321d0c1e11f672eeae7", size = 6763107, upload-time = "2026-02-18T16:47:11.784Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b5/e2a3c90aa1059f5b5f593379caad7be3cc3c2ce1ddfc7730e39854e174fe/psycopg_binary-3.3.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dde92cfde09293fb63b3f547919ba7d73bd2654573c03502b3263dd0218e44e", size = 5006494, upload-time = "2026-02-18T16:47:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3e/bf126e0a1f864e191b7f3eeea667ee2ce13d582b036255fb8b12946d1f7a/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78c9ce98caaf82ac8484d269791c1b403d7598633e0e4e2fa1097baae244e2f1", size = 4533850, upload-time = "2026-02-18T16:47:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d8/bb5e8d395deb945629aa0c65d12ab90ec3bfcbdf56be89e2a84d001864c9/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d593612758d0041cb13cb0003f7f8d3fabb7ad9319e651e78afae49b1cf5860e", size = 4223316, upload-time = "2026-02-18T16:47:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/c2/70/33eef61b0f0fd41ebf93b9699f44067313a45016827f67b3c8cc41f0a7ab/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f24e8e17035200a465c178e9ea945527ad0738118694184c450f1192a452ff25", size = 3954515, upload-time = "2026-02-18T16:47:30.434Z" }, + { url = "https://files.pythonhosted.org/packages/ea/db/27c2b3b9698e713e83e11e8540daa27516f9e90390ec21a41091cb15fcaf/psycopg_binary-3.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e7b607f0e14f2a4cf7e78a05ebd13df6144acfba87cb90842e70d3f125d9f53f", size = 4260274, upload-time = "2026-02-18T16:47:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3b/71e5d603059bf5474215f573a3e2d357a4e95672b26e04d41674400d4862/psycopg_binary-3.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b27d3a23c79fa59557d2cc63a7e8bb4c7e022c018558eda36f9d7c4e6b99a6e0", size = 3557375, upload-time = "2026-02-18T16:47:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/b389119dd754483d316805260f3e73cdcad97925839107cc7a296f6132b1/psycopg_binary-3.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a89bb9ee11177b2995d87186b1d9fa892d8ea725e85eab28c6525e4cc14ee048", size = 4609740, upload-time = "2026-02-18T16:47:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9976eef20f61840285174d360da4c820a311ab39d6b82fa09fbb545be825/psycopg_binary-3.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f7d0cf072c6fbac3795b08c98ef9ea013f11db609659dcfc6b1f6cc31f9e181", size = 4676837, upload-time = "2026-02-18T16:47:55.523Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f2/d28ba2f7404fd7f68d41e8a11df86313bd646258244cb12a8dd83b868a97/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:90eecd93073922f085967f3ed3a98ba8c325cbbc8c1a204e300282abd2369e13", size = 5497070, upload-time = "2026-02-18T16:47:59.929Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/6c5c54b815edeb30a281cfcea96dc93b3bb6be939aea022f00cab7aa1420/psycopg_binary-3.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dac7ee2f88b4d7bb12837989ca354c38d400eeb21bce3b73dac02622f0a3c8d6", size = 5172410, upload-time = "2026-02-18T16:48:05.665Z" }, + { url = "https://files.pythonhosted.org/packages/51/75/8206c7008b57de03c1ada46bd3110cc3743f3fd9ed52031c4601401d766d/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62cf8784eb6d35beaee1056d54caf94ec6ecf2b7552395e305518ab61eb8fd2", size = 6763408, upload-time = "2026-02-18T16:48:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/ea1641a1e6c8c8b3454b0fcb43c3045133a8b703e6e824fae134088e63bd/psycopg_binary-3.3.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a39f34c9b18e8f6794cca17bfbcd64572ca2482318db644268049f8c738f35a6", size = 5006255, upload-time = "2026-02-18T16:48:22.176Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/538df099bf55ae1637d52d7ccb6b9620b535a40f4c733897ac2b7bb9e14c/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:883d68d48ca9ff3cb3d10c5fdebea02c79b48eecacdddbf7cce6e7cdbdc216b8", size = 4532694, upload-time = "2026-02-18T16:48:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/00780c0e187ea3c13dfc53bd7060654b2232cd30df562aac91a5f1c545ac/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:cab7bc3d288d37a80aa8c0820033250c95e40b1c2b5c57cf59827b19c2a8b69d", size = 4222833, upload-time = "2026-02-18T16:48:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/a07f1ff713c51d64dc9f19f2c32be80299a2055d5d109d5853662b922cb4/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:56c767007ca959ca32f796b42379fc7e1ae2ed085d29f20b05b3fc394f3715cc", size = 3952818, upload-time = "2026-02-18T16:48:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/d33f268a7759b4445f3c9b5a181039b01af8c8263c865c1be7a6444d4749/psycopg_binary-3.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da2f331a01af232259a21573a01338530c6016dcfad74626c01330535bcd8628", size = 4258061, upload-time = "2026-02-18T16:48:41.365Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3b/0d8d2c5e8e29ccc07d28c8af38445d9d9abcd238d590186cac82ee71fc84/psycopg_binary-3.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:19f93235ece6dbfc4036b5e4f6d8b13f0b8f2b3eeb8b0bd2936d406991bcdd40", size = 3558915, upload-time = "2026-02-18T16:48:46.679Z" }, + { url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" }, + { url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" }, + { url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" }, + { url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" }, + { url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" }, + { url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" }, + { url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -386,8 +865,8 @@ wheels = [ [[package]] name = "pydantic-ai-slim" -version = "1.76.0" -source = { registry = "https://pypi.org/simple" } +version = "1.79.1.dev5+558d5ddb" +source = { git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=main#558d5ddb42c8772160ca42d39263b129cc3462d8" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "genai-prices" }, @@ -398,9 +877,13 @@ dependencies = [ { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/12/625331a88ea2db885e4cda4c2384f8dac9a876260ee3e6e982a950733e6c/pydantic_ai_slim-1.76.0.tar.gz", hash = "sha256:db82bc9a24f9c80d00be23f7a18e5cda8484d77c61a5cd8eedfc2fc8515657b2", size = 508214, upload-time = "2026-04-02T00:25:51.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/c6/7801af6853502bd53f00e88560f60270d8a2ab3bd8e19732d5ae8f261503/pydantic_ai_slim-1.76.0-py3-none-any.whl", hash = "sha256:1932799ff46a03e83fca3fb194f580dcbf3b24c9d2571ef64d0789c950499e23", size = 651190, upload-time = "2026-04-02T00:25:43.987Z" }, + +[package.optional-dependencies] +dbos = [ + { name = "dbos" }, +] +temporal = [ + { name = "temporalio" }, ] [[package]] @@ -523,18 +1006,14 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "1.76.0" -source = { registry = "https://pypi.org/simple" } +version = "1.79.1.dev5+558d5ddb" +source = { git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_graph&branch=main#558d5ddb42c8772160ca42d39263b129cc3462d8" } dependencies = [ { name = "httpx" }, { name = "logfire-api" }, { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/3c/6dc8c19c9eba073884b861d88cc96658d38bde4dd49f4b07e9a87f589eec/pydantic_graph-1.76.0.tar.gz", hash = "sha256:e0f8f85ab08b0f896aed50bc888f946f7c2ef3f032b78fefc8dc1fd77a49406e", size = 58716, upload-time = "2026-04-02T00:25:53.605Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/4f/60b018568a33c907734613fb089ff288168faa6affb634e05e1a53f1f9e8/pydantic_graph-1.76.0-py3-none-any.whl", hash = "sha256:eda23a36bcaf4ab09ce10e7860818c6eeb2f42b3429ba9a575cc3b713ef3bbd6", size = 72502, upload-time = "2026-04-02T00:25:47.148Z" }, -] [[package]] name = "pydantic-harness" @@ -543,10 +1022,23 @@ dependencies = [ { name = "pydantic-ai-slim" }, ] +[package.optional-dependencies] +code-mode = [ + { name = "pydantic-monty" }, +] +dbos = [ + { name = "pydantic-ai-slim", extra = ["dbos"] }, +] +temporal = [ + { name = "pydantic-ai-slim", extra = ["temporal"] }, +] + [package.dev-dependencies] dev = [ { name = "anyio", extra = ["trio"] }, { name = "coverage" }, + { name = "logfire", extra = ["httpx"] }, + { name = "pydantic-harness", extra = ["code-mode"] }, { name = "pytest" }, { name = "pytest-anyio" }, ] @@ -556,12 +1048,20 @@ lint = [ ] [package.metadata] -requires-dist = [{ name = "pydantic-ai-slim", specifier = ">=1.76.0" }] +requires-dist = [ + { name = "pydantic-ai-slim", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=main" }, + { name = "pydantic-ai-slim", extras = ["dbos"], marker = "extra == 'dbos'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=main" }, + { name = "pydantic-ai-slim", extras = ["temporal"], marker = "extra == 'temporal'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=main" }, + { name = "pydantic-monty", marker = "extra == 'code-mode'", specifier = ">=0.0.10" }, +] +provides-extras = ["code-mode", "dbos", "temporal"] [package.metadata.requires-dev] dev = [ { name = "anyio", extras = ["trio"] }, { name = "coverage" }, + { name = "logfire", extras = ["httpx"], specifier = ">=4.31.0" }, + { name = "pydantic-harness", extras = ["code-mode"] }, { name = "pytest" }, { name = "pytest-anyio" }, ] @@ -570,6 +1070,77 @@ lint = [ { name = "ruff", specifier = ">=0.14" }, ] +[[package]] +name = "pydantic-monty" +version = "0.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/c6/b6704b4d236fca5c3d9f94314406dd8b51c71939db764c3082da0ef68791/pydantic_monty-0.0.10.tar.gz", hash = "sha256:eb2ca00f076f6e85cf15e236d89f20d4c72f40a3d880990906ec154e46900d51", size = 949379, upload-time = "2026-04-09T10:42:49.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/ca/32c97328d967a9164c4005976b071fb103984048f1873e40485c80231086/pydantic_monty-0.0.10-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:703f2f988d2d09644c555a5e671e255e98c667cc3f30da75ceea2f0da34f6575", size = 7230772, upload-time = "2026-04-09T10:42:07.781Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/363b2a2e969314657580e8ce98ef1f5cc407448763d61e2430804236bb55/pydantic_monty-0.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb100951e16cd033d5a1558e04b40ffad6bb505e28a3fe423f48b3703a522158", size = 7230159, upload-time = "2026-04-09T10:42:05.829Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ad/4e2e27976f724aa477ccc81ec01220e8edc46a4a1f4e1e53fb8c37aa4f1b/pydantic_monty-0.0.10-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:de03978d2fdd00e83eee72498f889074dc940a3ad0cc630331d808d51556071d", size = 7771263, upload-time = "2026-04-09T10:42:14.201Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2e/55a8b7ccf0d5247981ed40384148a00b80baf1827165ac8005190512249b/pydantic_monty-0.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c52ed5ece0e612b16b7be02c40fec2a623d4c595312d909e62097022df3e39b2", size = 7017356, upload-time = "2026-04-09T10:42:25.195Z" }, + { url = "https://files.pythonhosted.org/packages/00/10/d806a4bd1a9bbde291bf40390e1d41ac9c0d55e5605e64047ed53373c9fa/pydantic_monty-0.0.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5cf27e91581f49362157ad1e056d39c9c5bb4c7b2bca3ca8808795417493cfa", size = 7310311, upload-time = "2026-04-09T10:41:32.236Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/46dd18aeb596497e9f7bbbd82ac1e5f1a1093a92c6cd8dc6371ada5cb67a/pydantic_monty-0.0.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eefb3ad20f94b38a61ff178a33c033c8308bb7f9331fa97389578b1e118af7a2", size = 7855836, upload-time = "2026-04-09T10:41:10.853Z" }, + { url = "https://files.pythonhosted.org/packages/28/60/f5d3e95fa369e286248c02d1187a7aa5e9a3e6d722b0f516276b54be6d73/pydantic_monty-0.0.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c9925a3cc8f6d41ecfe91d6cb63c1873945165d66895e9a5a81f341a07a773e", size = 8084707, upload-time = "2026-04-09T10:42:12.129Z" }, + { url = "https://files.pythonhosted.org/packages/c0/df/f296b7b9c27e575be117258ce796726278dae1a291a8b78df30eea1482f0/pydantic_monty-0.0.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a078a28444ff7c13f5820144dae1234a23f8f1dc8201aaa0bb930e6aa9e3e6ab", size = 7770624, upload-time = "2026-04-09T10:42:35.98Z" }, + { url = "https://files.pythonhosted.org/packages/f0/48/e2deaa7fc328eae2e96f3b60689e7102891eb72177046459ca0642fdd8cc/pydantic_monty-0.0.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:274a7fdd5760de4ee7119e4dfffb0d198ab0b8b896e75d3ee88be3822ae37017", size = 7197935, upload-time = "2026-04-09T10:41:57.193Z" }, + { url = "https://files.pythonhosted.org/packages/59/76/6f0a4218b32d1e5b4e45ebde45de280b3f36bb6efba9e56713c589a925cd/pydantic_monty-0.0.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a43eec5773e4dd78a743aeced8a3b1157b2af0f02b44958c793fd39dc9420639", size = 7652232, upload-time = "2026-04-09T10:42:16.528Z" }, + { url = "https://files.pythonhosted.org/packages/66/b2/fe6ae046bb2a60c086a894f896e119ec688c66a4da44eb9a945a38d817d4/pydantic_monty-0.0.10-cp310-cp310-win32.whl", hash = "sha256:ea65e2ab21a5568bd9b7f53fc799a9192e2d51245f49bbd3806f3f2dd7663dfb", size = 7150525, upload-time = "2026-04-09T10:41:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/f4/53/d067bec48628971e77a16dead2908c09a47ad9c5f27e792e90d72e84c15f/pydantic_monty-0.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:fc52a71bb974f43b4efe01ddbf8bec442f1b1d8ec8cb853c055320a0914afd02", size = 7935636, upload-time = "2026-04-09T10:42:01.535Z" }, + { url = "https://files.pythonhosted.org/packages/7a/8e/55b3df07d5926c9a156faa748af61910a2e03a457afbe6f86be47fc2953c/pydantic_monty-0.0.10-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c975285e6ffc2f3f487b17cab8c7d3c7068a43df084b343a4ef94fcc6626daa", size = 7229850, upload-time = "2026-04-09T10:41:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/54/5e/fe59f455574b01e72fd24b26e74aa9ba5c47e4b283c1534a395f1eb5a970/pydantic_monty-0.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:665cffba6c5f7eaf5d6f49efaca7790f542698b2af823e304e6026c346e408cf", size = 7228593, upload-time = "2026-04-09T10:41:47.52Z" }, + { url = "https://files.pythonhosted.org/packages/0c/84/463472f5307df4630ec491af2b2f1e76ff6cbd498cea37278fdfe883031d/pydantic_monty-0.0.10-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:555a91e1e2c1b2d564ac9d153e01ff3a00b455e171e8e4ca4dcfbc256d625a6d", size = 7770771, upload-time = "2026-04-09T10:41:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/04/63/ef84edd8a5524cf886c17caf65c76bc9f1af2259878e10411d3e655f40bd/pydantic_monty-0.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c60e25136fa96afadc024e4d8a6c73693ed7374ca148860e995f9b431ba21028", size = 7017007, upload-time = "2026-04-09T10:41:59.481Z" }, + { url = "https://files.pythonhosted.org/packages/0a/36/65aa920311d6179caccb1800d2c37f988b1922f7e09631760378a7236420/pydantic_monty-0.0.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b225152a5b85e198983c9cc589913a365e016de5b5a257b4ed173149ef52e706", size = 7309662, upload-time = "2026-04-09T10:42:18.876Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e7/316bb2e1c5d06700536500ccb3d0e5978c804fc9d0eee76e34f08736525b/pydantic_monty-0.0.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89e1916f0cd1d585d3201c6f583d5ed064c0017cab59aea6cbe22b7e0fb58613", size = 7856292, upload-time = "2026-04-09T10:41:49.437Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fc/4725bafefd20687599e0b2690fbebcf7039128588f72b81a385ed0da7704/pydantic_monty-0.0.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30764c8c28fdb0ab6a6dd9567781b4cb3810cc041e9d009dc0e95a0150b7b16f", size = 8083381, upload-time = "2026-04-09T10:42:47.382Z" }, + { url = "https://files.pythonhosted.org/packages/76/2c/481b5ed7ad1ca267938f40d9aaf2df3fc02283ca5908be958f8764e574f3/pydantic_monty-0.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1565c30367fe6671dcb6ce246d6e102799e62b20cc7aec98c90291ff3846ae51", size = 7769468, upload-time = "2026-04-09T10:41:14.756Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/97f5ff26ddd91e4fdeb1a8a434b224e7acc35a9e856624814ad9b8114cf0/pydantic_monty-0.0.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cb2cf4bff3a293448ecd762d1ff38ecebb270f934d0dddbfe9e9c2f7a17a562", size = 7197465, upload-time = "2026-04-09T10:43:00.086Z" }, + { url = "https://files.pythonhosted.org/packages/dd/24/954a06e8f4942c92c8a0f624964b1babeaa53c06b0a3c6c96130d00d5378/pydantic_monty-0.0.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8a7e609b649075540049bc87cd8356c74df64a1ca9dd03a1fe4a68c769d67152", size = 7651668, upload-time = "2026-04-09T10:42:53.073Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d3/f9bfc7996a0f94a7cc2304634d92fd5e1120c9180799ecbe35ba368bc07d/pydantic_monty-0.0.10-cp311-cp311-win32.whl", hash = "sha256:8671f8c49e882c7c380c94b31ae529c30ffbb13034c081b9ec0b42ac1500e342", size = 7151445, upload-time = "2026-04-09T10:41:12.876Z" }, + { url = "https://files.pythonhosted.org/packages/87/1a/d42dbf0096c0232fa721c777a027aa3c1376c7a472aeb4160dc7f57e34ca/pydantic_monty-0.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:80e079f60e946baae2e40f9cd0831c84ee705c5ee84d0b55e6246dee890a9f4c", size = 7934688, upload-time = "2026-04-09T10:41:42.604Z" }, + { url = "https://files.pythonhosted.org/packages/d1/47/f8907a47f8964bdc00c0a65c0d9c9c9f2ad848dccbc5f1dd3fd5ecd7d221/pydantic_monty-0.0.10-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:508d68019e53d8fa2363f754b95a136d6034db4a831a85fc45887ed0aa24ddb9", size = 7232388, upload-time = "2026-04-09T10:40:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/baf7bc2a783fad881af3268c6e3800dd1adcffeeec6e56926c72e44ed657/pydantic_monty-0.0.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c66cd5f91af3c9d459d6d65944a4b5c8417cd2003b9e6bde6a4e49ebb2bf92", size = 7197861, upload-time = "2026-04-09T10:42:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/66/1a/93235013ed7e5c24d426cbcc2ec878a8f4fc91dae06efd8e5ba6600d3901/pydantic_monty-0.0.10-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1aaac65c988286606b707ec0c6132ec32ffbfba02579c923729170ef01ccd87f", size = 7774438, upload-time = "2026-04-09T10:41:34.415Z" }, + { url = "https://files.pythonhosted.org/packages/59/9e/c30f7485b5010dcb681d2f257ff2601968bfe57731b3673455ff5f7b5ac6/pydantic_monty-0.0.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0443632d1f4e89c8b27687d0d1505ec630f9ffd74708d6f9359e99f1fd09b0c2", size = 7020254, upload-time = "2026-04-09T10:41:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/bf/7f/bb0e0275a0d26fe6237249c859dfc808a0d7779d25343c3d61ddb6e24bdb/pydantic_monty-0.0.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:246e17c21e441608e2fa834b2f41a70b3cc4b3bcb5d0967ab24a650bfa2a413a", size = 7317175, upload-time = "2026-04-09T10:42:43.179Z" }, + { url = "https://files.pythonhosted.org/packages/7a/52/746c169a65c5c6d561f00a16e5ec42db5a13ac0d631ed0427c3d49abb628/pydantic_monty-0.0.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48a7cd06200e2c73b3f9f4aa20ed4961c7a11a10a29315ae26748238a2f7a05e", size = 7858261, upload-time = "2026-04-09T10:41:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2d/eaef9d70a4d5be0be210257cafd9b0f4d0969001f593215bb4256dc1164d/pydantic_monty-0.0.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1f020acdf0bd517a3fb017f84d592911d00819fc3aab43e57ab47c556a333e9", size = 8093965, upload-time = "2026-04-09T10:40:50.351Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/d59eef69f76203f6c9a716388346d0349ab2152b4ce097373a7d09cc0d92/pydantic_monty-0.0.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1669bc1aa31dd93f5dddc480a8a9f992bfaeafebee930f6e8237b55355d0c893", size = 7729127, upload-time = "2026-04-09T10:41:54.221Z" }, + { url = "https://files.pythonhosted.org/packages/3c/c2/8da494d35a31f7ee5c814d4605d129e15468ab70e824af4f05c93e38fee6/pydantic_monty-0.0.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f2aad421e85b1b4c51039ecdaa61e64c2a3090e02bc93927daf735bc205e621d", size = 7200825, upload-time = "2026-04-09T10:41:00.202Z" }, + { url = "https://files.pythonhosted.org/packages/fd/50/66887700e9471cc0c2cd5e0699dfcb3283c8c02b56723993c3a44d3335f4/pydantic_monty-0.0.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ee20dab89285d3d74045894afb5b21eb582fccdb1a0950a25a5f466f147058e9", size = 7658093, upload-time = "2026-04-09T10:41:40.567Z" }, + { url = "https://files.pythonhosted.org/packages/db/c3/06ebc4471445f549fb32da933d3f5df2ee303cfbada188e6b36a67406251/pydantic_monty-0.0.10-cp312-cp312-win32.whl", hash = "sha256:e1f17ceb14483d42991cf9d7fee3449487c19d57534d9273ef74473381dcc714", size = 7146871, upload-time = "2026-04-09T10:42:50.817Z" }, + { url = "https://files.pythonhosted.org/packages/cc/42/ee0fa0d70502c15205812ab433979b55ae152f21c2ae32ca225dfabc7873/pydantic_monty-0.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:37141d0af9e55c1cbb5d7485392662e729ac1d50393c12cb275272aab7081d49", size = 7900231, upload-time = "2026-04-09T10:41:29.911Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c4/366d8d22ab2a7f8c8539a5b128f3fef3562358f988aee23f365bf5a83906/pydantic_monty-0.0.10-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:737d521a273f8bfc329db8f0f963f4bc26965a5fbaf6360bff2875fc45a66e88", size = 7231252, upload-time = "2026-04-09T10:41:36.206Z" }, + { url = "https://files.pythonhosted.org/packages/cc/84/d5e3dd7ea784aee4b3098452d24002bda4198146ff3f38637727ea63446b/pydantic_monty-0.0.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d5f2e83d18a3199e3ba5283da31eb2522da97a8192626c9bc99a6c7aa1e1a80", size = 7198247, upload-time = "2026-04-09T10:41:21.039Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/97ec74a6f83cffbcef87082c3bac1cdf09b397956763a802c66158b6465e/pydantic_monty-0.0.10-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d340d94775989cbf5308b56f61c549f49172a218dd85470d018d396495ffac2b", size = 7773876, upload-time = "2026-04-09T10:41:06.506Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/d986c07b1ea7e9cf70b6ee8ee30768883ed9f280e9080121437355671306/pydantic_monty-0.0.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3e6038aed55e2b07c1226d4d52a47362c071d19df14f72bbe9a3ecedc7d59b", size = 7020099, upload-time = "2026-04-09T10:42:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/dc/58/14cd67d79c7aa728c9284491b80bdc965979bdb7bf3bebedc4adb61ca82f/pydantic_monty-0.0.10-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37a0a8de1c9a96a4cf0a202533781ced03557a7136c72a71198a309f7d131c4c", size = 7317066, upload-time = "2026-04-09T10:42:29.585Z" }, + { url = "https://files.pythonhosted.org/packages/36/39/63e5f3dbb5a28362831125259a2046abd085865d4e6bec7d03a744197020/pydantic_monty-0.0.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4860a86dc23503dcad90b0a15ed1948684477122a787dd4722529b979e49418", size = 7857493, upload-time = "2026-04-09T10:42:20.755Z" }, + { url = "https://files.pythonhosted.org/packages/9f/89/03a68c063d3e035343de76fed017fac388bbb40f7b146e3be5cff6f91452/pydantic_monty-0.0.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95b897b029ee469098ab1f612ba700d8b8a86fc7c7700a645f91e198871fc9f7", size = 8092357, upload-time = "2026-04-09T10:40:53.386Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/dee907088fa523c70dbe21a42f2bfb7695f045283b4da3d8b21653ab6945/pydantic_monty-0.0.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:868016a061a7ba8976818011241271a55120858a74a9446e39e799e97b0af572", size = 7729142, upload-time = "2026-04-09T10:42:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/a9f2f76c6d4e859d03d5584a0e3e713ff1bdce3dbe7ac4dba70b8238d62b/pydantic_monty-0.0.10-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bd59cf3b3b2839aaac0fb8627d6d44f87353363a48c343373c52262bf1a56366", size = 7200482, upload-time = "2026-04-09T10:41:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/a5/cb/2129b76698aca444f87b9253c3feda929496f430517cf183c3474b5371e2/pydantic_monty-0.0.10-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:50b65da20988056ebd60e8bd89d43a1fa63b2c252bdc0f92b7f394ec8e581098", size = 7656158, upload-time = "2026-04-09T10:42:57.637Z" }, + { url = "https://files.pythonhosted.org/packages/4f/26/d084fe03be73c583a1c1c0b049caf34ab628f3d5a942e897518e2b0e3f83/pydantic_monty-0.0.10-cp313-cp313-win32.whl", hash = "sha256:5f1861ffd6b2e998f2a2a974bdca924f7a9307db64a0dfbb0eb3880fbc676547", size = 7146075, upload-time = "2026-04-09T10:42:31.466Z" }, + { url = "https://files.pythonhosted.org/packages/6b/18/f752f3b9a94976216ea7b0300b7b30c16b939380f7776ee79d650f39124b/pydantic_monty-0.0.10-cp313-cp313-win_amd64.whl", hash = "sha256:75bb9cf6c9934d2eed453310b88e66f39271a07bdc2bb81a1c7eb3467baf6686", size = 7900789, upload-time = "2026-04-09T10:42:27.687Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8a/6f1d7c0ca732001216f2b0f1e0e97c084a5879b8f0e47c766a5ea1c4fcb7/pydantic_monty-0.0.10-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:03bece36f6de62e8226cbb773070bca8f2babf14be8d7f222f12725155251656", size = 7233909, upload-time = "2026-04-09T10:40:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/0639e40bc3d98e32c9bd71e1a92c87632d159dc2270216dc9fb3153f52d2/pydantic_monty-0.0.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:be3306c29b707a47eb07fceac88ee9c9fa2817577e752f6c03f6fcf2506860c7", size = 7221398, upload-time = "2026-04-09T10:41:02.023Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8f/2d835ed5972ae5044266f1cc826e726c8e62c1a46308e5cfafefbf2a87ab/pydantic_monty-0.0.10-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:07030d7118a536ae77a8c538f245869d53837f7eb9a638bbd701a0dd47534190", size = 7776139, upload-time = "2026-04-09T10:41:16.948Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6a/e581741d661be56e0e80cfa19869fd5efcf5e24b893d2cefea2f1df16e1a/pydantic_monty-0.0.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b0837306e8225e6fd610ab10023942e54a9f2483a103f3e9151c9ebc30dd9f9", size = 7020149, upload-time = "2026-04-09T10:42:40.764Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1c/de7d39da5c41cd98fb0d6ac76f0109e5d4e3885514d6527336b93dcc7383/pydantic_monty-0.0.10-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:479bf7faabcb43b997556c698263cca7ea124772098bd595d72321ce493d1386", size = 7319409, upload-time = "2026-04-09T10:43:02.411Z" }, + { url = "https://files.pythonhosted.org/packages/f5/40/825a58995b2615390f78e43ff908f29a91a888774a2d37ee569215649bd7/pydantic_monty-0.0.10-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d72e20f41ca6be2136754b777c2f644827e5a70d8df48b5bcadd98c67bc6e336", size = 7858377, upload-time = "2026-04-09T10:42:10.115Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/65efc128de7bd716644903713f69194ff7e68073da168c99cf555912ff9e/pydantic_monty-0.0.10-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b6a7e4f152eba2145980638aa1a2aae1d3d5c041af9de70a907e46a24ae990f", size = 8095036, upload-time = "2026-04-09T10:41:04.054Z" }, + { url = "https://files.pythonhosted.org/packages/15/b5/763abefa6a7b17e0ff80c375e1671058e87f3efe2fa26fb0a31bf9bfd901/pydantic_monty-0.0.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:982d347b530816e6e73ace02848673319d4996bd88c1db1cd61e2f095c9bca90", size = 7757308, upload-time = "2026-04-09T10:42:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/e13cf79bed54637d468d0fb12cf36fd6509c8874b9f35c173a0c60b5ef05/pydantic_monty-0.0.10-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:b16be5e2a32cbf0e4bf67432972300d60521664dde7f956b1ad85f617ce7d125", size = 7200578, upload-time = "2026-04-09T10:41:25.381Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cc/4716daf2d1707191d8695026249933913ba0c7c27479277c45b86006337f/pydantic_monty-0.0.10-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2a93971e42ea2ebe05f89db1a4476a835f4f29e998255b0558f88f03344b3403", size = 7659299, upload-time = "2026-04-09T10:42:45.248Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c2/5946115b5fc6736a537ac30fc10afea465bc82b24a607c8b6d063f60e79b/pydantic_monty-0.0.10-cp314-cp314-win32.whl", hash = "sha256:c1a092e4b9abab8f77bd3e3d4219e5236c899023a46bbcbf1e920d8f8154f67b", size = 7147583, upload-time = "2026-04-09T10:41:18.977Z" }, + { url = "https://files.pythonhosted.org/packages/36/ee/39f801138bc5e76bd3cf34bc58b10465d8eac59c0c6ae787011aff9dbe00/pydantic_monty-0.0.10-cp314-cp314-win_amd64.whl", hash = "sha256:51651095ccf5cdde88cbea47887aae01b102527b33331c7c6ccb64046f05b1d0", size = 7925059, upload-time = "2026-04-09T10:42:03.424Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -623,6 +1194,110 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/25/bd6493ae85d0a281b6a0f248d0fdb1d9aa2b31f18bcd4a8800cf397d8209/pytest_anyio-0.0.0-py2.py3-none-any.whl", hash = "sha256:dc8b5c4741cb16ff90be37fddd585ca943ed12bbeb563de7ace6cd94441d8746", size = 1999, upload-time = "2021-06-29T22:57:29.158Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +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" } +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" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + [[package]] name = "ruff" version = "0.15.7" @@ -648,6 +1323,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -666,6 +1359,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/76/f908955139842c362aa877848f42f9249642d5b69e06cee9eae5111da1bd/sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f", size = 2159321, upload-time = "2026-04-03T16:50:11.8Z" }, + { url = "https://files.pythonhosted.org/packages/24/e2/17ba0b7bfbd8de67196889b6d951de269e8a46057d92baca162889beb16d/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b", size = 3238937, upload-time = "2026-04-03T16:54:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/90/1e/410dd499c039deacff395eec01a9da057125fcd0c97e3badc252c6a2d6a7/sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1", size = 3237188, upload-time = "2026-04-03T16:56:53.217Z" }, + { url = "https://files.pythonhosted.org/packages/ab/06/e797a8b98a3993ac4bc785309b9b6d005457fc70238ee6cefa7c8867a92e/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339", size = 3190061, upload-time = "2026-04-03T16:54:47.489Z" }, + { url = "https://files.pythonhosted.org/packages/44/d3/5a9f7ef580af1031184b38235da6ac58c3b571df01c9ec061c44b2b0c5a6/sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d", size = 3211477, upload-time = "2026-04-03T16:56:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/7be8c8cb35f038e963a203e4fe5a028989167cc7299927b7cf297c271e37/sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3", size = 2119965, upload-time = "2026-04-03T17:00:50.009Z" }, + { url = "https://files.pythonhosted.org/packages/b5/31/0defb93e3a10b0cf7d1271aedd87251a08c3a597ee4f353281769b547b5a/sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75", size = 2142935, upload-time = "2026-04-03T17:00:51.675Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/e3617cc67420f8f403efebd7b043128f94775e57e5b84e7255203390ceae/sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe", size = 2159126, upload-time = "2026-04-03T16:50:13.242Z" }, + { url = "https://files.pythonhosted.org/packages/20/9b/91ca80403b17cd389622a642699e5f6564096b698e7cdcbcbb6409898bc4/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014", size = 3315509, upload-time = "2026-04-03T16:54:49.332Z" }, + { url = "https://files.pythonhosted.org/packages/b1/61/0722511d98c54de95acb327824cb759e8653789af2b1944ab1cc69d32565/sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536", size = 3315014, upload-time = "2026-04-03T16:56:56.376Z" }, + { url = "https://files.pythonhosted.org/packages/46/55/d514a653ffeb4cebf4b54c47bec32ee28ad89d39fafba16eeed1d81dccd5/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88", size = 3267388, upload-time = "2026-04-03T16:54:51.272Z" }, + { url = "https://files.pythonhosted.org/packages/2f/16/0dcc56cb6d3335c1671a2258f5d2cb8267c9a2260e27fde53cbfb1b3540a/sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700", size = 3289602, upload-time = "2026-04-03T16:56:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/51/6c/f8ab6fb04470a133cd80608db40aa292e6bae5f162c3a3d4ab19544a67af/sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a", size = 2119044, upload-time = "2026-04-03T17:00:53.455Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/55a6d627d04b6ebb290693681d7683c7da001eddf90b60cfcc41ee907978/sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af", size = 2143642, upload-time = "2026-04-03T17:00:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "temporalio" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nexus-rpc" }, + { name = "protobuf" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "types-protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/9c/3782bab0bf11a40b550147c19a5d1a476c17405391751982408902d9f138/temporalio-1.25.0.tar.gz", hash = "sha256:a3bbec1dcc904f674402cfa4faae480fda490b1c53ea5440c1f1996c562016fb", size = 2152534, upload-time = "2026-04-08T18:53:55.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/e3/5676dd10d1164b6d6ca8752314054097b89c5da931e936af402a7b15236c/temporalio-1.25.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6dc1bc8e1773b1a833d86a7ede2dd90ef4e031ced5b748b59e7f09a5bf9b327d", size = 13943906, upload-time = "2026-04-08T18:53:30.022Z" }, + { url = "https://files.pythonhosted.org/packages/89/50/7cbf7f845973be986ec165348f72f7a409750842a04d554965a39be5cb4f/temporalio-1.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3c8fdcf79ea5ae8ae2cf6f48072e4a86c3e0f4778f6a8a066c6ff1d336587db4", size = 13298719, upload-time = "2026-04-08T18:53:35.95Z" }, + { url = "https://files.pythonhosted.org/packages/d2/31/d474bab8535552add6ed289911bf1ffae5d7071823ece1069842190fcaed/temporalio-1.25.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:141f37aaafd7d090ba5c8776e4e9bc60df1fbc64b9f50c8f00e905a436588ddc", size = 13555435, upload-time = "2026-04-08T18:53:41.36Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c8/e7dc053d6107bf2a037a3c9fe7b86639a25dcb888bde0e1ca366901ee47f/temporalio-1.25.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7ca5bb80264976477d4dc7a839b3d22af8577ae92306526a061481db49bf92", size = 14052050, upload-time = "2026-04-08T18:53:46.44Z" }, + { url = "https://files.pythonhosted.org/packages/08/70/9340ed3a578321cbc153041d34834bb1ec3f1f3e3d9cded47cd1b7c3e403/temporalio-1.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9411534279a2e64847231b6059c214bff4d57cfd1532bd09f333d0b1603daa7f", size = 14299684, upload-time = "2026-04-08T18:53:52.482Z" }, +] + [[package]] name = "tomli" version = "2.4.0" @@ -738,6 +1511,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/93/dab25dc87ac48da0fe0f6419e07d0bfd98799bed4e05e7b9e0f85a1a4b4b/trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b", size = 510294, upload-time = "2026-02-14T18:40:53.313Z" }, ] +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20260221" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -759,6 +1568,161 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +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" } +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" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + [[package]] name = "zipp" version = "3.23.0"