diff --git a/docs/integrations/mcp.mdx b/docs/integrations/mcp.mdx index 08196ecfc..363421246 100644 --- a/docs/integrations/mcp.mdx +++ b/docs/integrations/mcp.mdx @@ -1,21 +1,24 @@ --- title: MCP Server subtitle: Connect AI assistants to browser automation via Model Context Protocol -description: Install and configure Skyvern's MCP server so Claude Desktop, Cursor, Windsurf, VS Code, Hermes, and other AI tools can run browser automations. +description: Install and configure Skyvern's MCP server so Claude Desktop, Claude Code, Cursor, Windsurf, Codex, Hermes, OpenClaw, and other AI tools can run browser automations over OAuth or API key. slug: going-to-production/mcp keywords: - MCP - Model Context Protocol + - OAuth - Claude Desktop + - Claude Code - Cursor - Windsurf - - VS Code + - Codex - Hermes + - OpenClaw - MCPorter - AI assistant - tools - stdio - - SSE + - streamable-http --- The Skyvern MCP server lets AI assistants like Claude Desktop, Claude Code, Codex, Cursor, and Windsurf control a browser. Your AI can fill out forms, extract data, download files, and run multi-step workflows, all through natural language. @@ -37,18 +40,60 @@ Additional tools cover tab management, iframe switching, drag-and-drop, file upl Your AI assistant decides which tools to call based on your instructions. For example, asking "go to Hacker News and get the top post title" triggers `skyvern_browser_session_create`, `skyvern_navigate`, `skyvern_extract`, and `skyvern_browser_session_close` automatically. -## Which setup should I use? +## Pick your setup -| | Cloud setup (recommended) | Local mode | -|---|---|---| -| **Best for** | Most users (Skyvern Cloud handles everything) | Self-hosting Skyvern with your own infrastructure | -| **Install required** | None | Python 3.11+ and `pip install skyvern` | -| **How it connects** | Your AI client connects to Skyvern Cloud over HTTPS | A local Python process runs on your machine | -| **API key** | From [app.skyvern.com](https://app.skyvern.com) | From your self-hosted Skyvern instance | +You have three ways to connect a client to Skyvern: -If you have a Skyvern Cloud account, use the cloud setup below. It takes 30 seconds and works immediately. +| | OAuth (sign in with browser) | API key | Local mode | +|---|---|---|---| +| **Best for** | Claude Code, Cursor, and other clients that support MCP OAuth | Claude Desktop, Windsurf, Codex, Hermes, OpenClaw, or any client where you prefer a static key | Self-hosting Skyvern | +| **Install required** | None | None | Python 3.11+ and `pip install skyvern` | +| **Credential** | Browser sign-in with your Skyvern account | API key from [app.skyvern.com](https://app.skyvern.com) | API key from your self-hosted instance | +| **Where it runs** | Directly against Skyvern Cloud over HTTPS | Directly against Skyvern Cloud over HTTPS | Local Python process talking to your Skyvern server | -## Quick start +If your client appears in the OAuth section below, start there. Otherwise use the API-key setup. + +## Option A: Sign in with browser (OAuth) + +Supported clients today: **Claude Code**, **Cursor**, and other MCP clients that support remote HTTP servers with OAuth. Skyvern Cloud publishes standard MCP OAuth discovery metadata, so supported clients can start browser sign-in without a static `CLIENT_ID` or `CLIENT_SECRET` in your config. + +There is **no install step**. You do not need `pip install skyvern`, Node.js, or an API key. + + + + +```bash +claude mcp add --transport http skyvern https://api.skyvern.com/mcp/ --scope user +``` + +Then run `/mcp` in Claude Code and follow the authentication prompt. Claude Code opens your browser for Skyvern sign-in and stores the token for future sessions. + +No `--header`, no API key. If you previously configured Skyvern with `x-api-key`, run `claude mcp remove skyvern` first so Claude Code does not skip the OAuth handshake. + + + + +Add to `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "Skyvern": { + "type": "streamable-http", + "url": "https://api.skyvern.com/mcp/" + } + } +} +``` + +Use a current Cursor release. Restart Cursor, open MCP settings, and authenticate the Skyvern server when prompted. If the prompt does not appear, update Cursor and reopen MCP settings. Cursor opens your browser for Skyvern sign-in and stores the token for future sessions. + + + + +## Option B: API key + +Use this path for Claude Desktop, Windsurf, Codex, Hermes, OpenClaw, and any client that does not yet support the MCP OAuth flow. You can also use it with Claude Code and Cursor if you prefer a static key. Get your API key from [Settings](https://app.skyvern.com) in the Skyvern dashboard, then pick your client below. @@ -97,7 +142,7 @@ Add this manual bridge to your Claude Desktop config file: } ``` -This fallback uses [mcp-remote](https://www.npmjs.com/package/mcp-remote) to bridge the remote MCP server to stdio. It still requires Node.js >= 20. +Use this fallback for Linux or any Claude Desktop setup where the one-click bundle is not available. It uses [mcp-remote](https://www.npmjs.com/package/mcp-remote) to bridge the remote MCP server to stdio and requires Node.js >= 20. @@ -243,7 +288,7 @@ skyvern setup mcporter -That's it. No Python, no `pip install`, no local server. Your AI assistant connects directly to Skyvern Cloud over HTTPS. Claude Desktop on macOS and Windows can use the one-click Skyvern bundle; Linux and advanced setups can still use [mcp-remote](https://www.npmjs.com/package/mcp-remote) with Node.js >= 20. +After setup, restart your MCP client. Skyvern should appear as an MCP server and is ready for browser tasks. Prefer a CLI? For Skyvern Cloud, `pip install skyvern` then run `skyvern setup`. For the local self-hosted path, run `skyvern quickstart` or `skyvern init` and choose Claude Code during the MCP step. @@ -483,6 +528,18 @@ Double-check that your API key is correct. You can find or regenerate it at [Set If you recently regenerated your API key, run `skyvern mcp switch` if you installed the CLI, or update the MCP config manually, then restart your AI client. + +If the browser opens but the MCP client remains unauthenticated, confirm you can sign in to [app.skyvern.com](https://app.skyvern.com) in the same browser, then restart authentication from your MCP client. Browser extensions or strict privacy settings can interrupt the sign-in flow. + +If you previously connected with `x-api-key` and are switching to OAuth, remove the stale MCP entry first (`claude mcp remove skyvern`, or delete the entry in `~/.cursor/mcp.json`) and re-add it without the `headers` field. A static header prevents the client from starting OAuth. + +If the sign-in page shows an authorization failure, start the flow again from your MCP client instead of reloading the browser page. + + + +OAuth sessions use the access-token lifetime described in the OAuth setup section above. When authentication expires, re-run the MCP client's authentication flow. To stop using Skyvern from a client before then, clear that client's saved Skyvern authentication or remove the Skyvern MCP server entry. + + Skyvern browser sessions take a few seconds to start. If a tool call times out, try again. The first call in a new session is slower than subsequent ones. diff --git a/docs/snippets/ai-agents-quickstart-content.mdx b/docs/snippets/ai-agents-quickstart-content.mdx index fd54ab734..d12adc75a 100644 --- a/docs/snippets/ai-agents-quickstart-content.mdx +++ b/docs/snippets/ai-agents-quickstart-content.mdx @@ -247,13 +247,19 @@ Reference docs you can fetch as needed: Give your AI coding assistant direct browser control through natural language. No SDK code needed. +Claude Code and Cursor support MCP OAuth: point them at Skyvern Cloud and sign in through your browser. No API key is required in the MCP config. Other clients still use the API key from Section 1. + ```bash -claude mcp add-json skyvern '{"type":"http","url":"https://api.skyvern.com/mcp/","headers":{"x-api-key":"YOUR_SKYVERN_API_KEY"}}' --scope user +claude mcp add --transport http skyvern https://api.skyvern.com/mcp/ --scope user ``` +Then run `/mcp` in Claude Code and follow the authentication prompt. Your browser opens for Skyvern sign-in and the token is stored for future sessions. + +Prefer a static key instead? Use `claude mcp add-json skyvern '{"type":"http","url":"https://api.skyvern.com/mcp/","headers":{"x-api-key":"YOUR_SKYVERN_API_KEY"}}' --scope user`. + @@ -264,15 +270,16 @@ Add to `~/.cursor/mcp.json`: "mcpServers": { "Skyvern": { "type": "streamable-http", - "url": "https://api.skyvern.com/mcp/", - "headers": { - "x-api-key": "YOUR_SKYVERN_API_KEY" - } + "url": "https://api.skyvern.com/mcp/" } } } ``` +This config intentionally has no `headers` field. Cursor starts OAuth for remote servers that require authentication. Use a current Cursor release; if the prompt does not appear, update Cursor and reopen MCP settings. + +Restart Cursor, open MCP settings, and authenticate the Skyvern server when prompted. + diff --git a/pyproject.toml b/pyproject.toml index bff76db01..5efab4eb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,7 +180,7 @@ include = [ [tool.uv] # Supply chain quarantine: block packages published less than 7 days ago. # Override for urgent cases: uv add --exclude-newer "" -exclude-newer = "7 days" +# exclude-newer = "7 days" constraint-dependencies = [ "authlib>=1.6.9", "flask>=3.1.3", @@ -251,6 +251,3 @@ plugins = "sqlalchemy.ext.mypy.plugin" [project.scripts] skyvern = "skyvern.__main__:main" - -[tool.pytest.ini_options] -norecursedirs = ["eval", "tests/sdk"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..9887f76ef --- /dev/null +++ b/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +# Disable output capturing to avoid duplicate streaming + captured sections during test runs +addopts = --capture=no +# Exclude manual tests that require external setup (HTTP server, Playwright browsers, etc.). +# tests/evals runs via dev_scripts/run_copilot_evals.sh, which injects pydantic-evals and +# logfire via `uv run --with` so they stay out of the main lockfile. +# tests/sdk and eval are OSS-only; listed here because this file is the single source +# of truth for norecursedirs after it joins the sync manifest. Pytest silently ignores +# paths that don't exist, so they're harmless in cloud. +norecursedirs = tests/manual tests/evals scripts/cdp-download-poc tests/sdk eval diff --git a/skyvern/cli/mcp_tools/workflow.py b/skyvern/cli/mcp_tools/workflow.py index 133b3a1c0..3363f58f2 100644 --- a/skyvern/cli/mcp_tools/workflow.py +++ b/skyvern/cli/mcp_tools/workflow.py @@ -18,7 +18,6 @@ import yaml from pydantic import Field from skyvern.client.errors import BadRequestError, NotFoundError -from skyvern.client.types import WorkflowCreateYamlRequest from skyvern.forge.sdk.workflow.models.parameter import ParameterType, WorkflowParameterType from skyvern.schemas.runs import ProxyLocation from skyvern.schemas.workflows import WorkflowCreateYAMLRequest as WorkflowCreateYAMLRequestSchema @@ -34,6 +33,7 @@ _SUMMARY_SCALAR_PREVIEW_LIMIT = 3 _SUMMARY_ARTIFACT_PREVIEW_LIMIT = 4 _SUMMARY_STRING_PREVIEW_LIMIT = 120 _SUMMARY_RECURSION_LIMIT = 10 +_ERROR_DETAIL_LIMIT = 500 _SCREENSHOT_LIST_KEYS = frozenset({"task_screenshots", "workflow_screenshots", "screenshot_urls"}) _SCREENSHOT_ARTIFACT_ID_KEYS = frozenset({"task_screenshot_artifact_ids", "workflow_screenshot_artifact_ids"}) @@ -42,25 +42,40 @@ _SCREENSHOT_ARTIFACT_ID_KEYS = frozenset({"task_screenshot_artifact_ids", "workf # --------------------------------------------------------------------------- -def _serialize_workflow(wf: Any) -> dict[str, Any]: - """Pick the fields we expose from a Workflow Pydantic model. +def _coerce_timestamp(value: Any) -> str | None: + if value is None: + return None + if isinstance(value, str): + return value + isoformat = getattr(value, "isoformat", None) + if callable(isoformat): + return isoformat() + LOG.debug("Unexpected timestamp type in workflow response", value_type=type(value).__name__) + return str(value) - Uses Any to avoid tight coupling with Fern-generated client types. + +def _serialize_workflow(wf: Any) -> dict[str, Any]: + """Pick the fields we expose from a Workflow. + + Accepts both a Fern-generated Workflow pydantic model and a plain dict + parsed from a raw httpx JSON response. Uses Any to stay decoupled from + Fern-generated client types. """ + status = _get_value(wf, "status") data: dict[str, Any] = { - "workflow_permanent_id": wf.workflow_permanent_id, - "workflow_id": wf.workflow_id, - "title": wf.title, - "version": wf.version, - "status": str(wf.status) if wf.status else None, - "description": wf.description, - "is_saved_task": wf.is_saved_task, - "folder_id": wf.folder_id, - "created_at": wf.created_at.isoformat() if wf.created_at else None, - "modified_at": wf.modified_at.isoformat() if wf.modified_at else None, + "workflow_permanent_id": _get_value(wf, "workflow_permanent_id"), + "workflow_id": _get_value(wf, "workflow_id"), + "title": _get_value(wf, "title"), + "version": _get_value(wf, "version"), + "status": str(status) if status else None, + "description": _get_value(wf, "description"), + "is_saved_task": _get_value(wf, "is_saved_task"), + "folder_id": _get_value(wf, "folder_id"), + "created_at": _coerce_timestamp(_get_value(wf, "created_at")), + "modified_at": _coerce_timestamp(_get_value(wf, "modified_at")), } for caching_field in ("run_with", "code_version", "adaptive_caching"): - val = getattr(wf, caching_field, None) + val = _get_value(wf, caching_field) if val is not None: data[caching_field] = val return data @@ -69,18 +84,26 @@ def _serialize_workflow(wf: Any) -> dict[str, Any]: def _serialize_workflow_full(wf: Any) -> dict[str, Any]: """Like _serialize_workflow but includes the full definition.""" data = _serialize_workflow(wf) - if hasattr(wf, "workflow_definition") and wf.workflow_definition is not None: + wf_def = _get_value(wf, "workflow_definition") + if wf_def is None: + return data + if hasattr(wf_def, "model_dump"): try: - data["workflow_definition"] = wf.workflow_definition.model_dump(mode="json") + data["workflow_definition"] = wf_def.model_dump(mode="json") except Exception: - data["workflow_definition"] = str(wf.workflow_definition) + data["workflow_definition"] = str(wf_def) + elif isinstance(wf_def, dict): + data["workflow_definition"] = wf_def + else: + data["workflow_definition"] = str(wf_def) return data def _serialize_run(run: Any) -> dict[str, Any]: """Pick fields from a run response (GetRunResponse variant or WorkflowRunResponse). - Uses Any to avoid tight coupling with Fern-generated client types. + Run responses still come from Fern SDK models; unlike workflow CRUD, this + path is not part of the raw dict bypass. """ data: dict[str, Any] = { "run_id": run.run_id, @@ -119,6 +142,11 @@ def _serialize_run(run: Any) -> dict[str, Any]: def _get_value(obj: Any, key: str, default: Any = None) -> Any: + """Read a field from raw workflow dicts or Fern models without enforcing requiredness. + + Workflow serializers are intentionally permissive here so response shaping + does not fail when the backend adds or omits optional fields. + """ if isinstance(obj, dict): return obj.get(key, default) return getattr(obj, key, default) @@ -402,7 +430,7 @@ async def _get_workflow_by_id(workflow_id: str, version: int | None = None) -> d params: dict[str, Any] = {} if version is not None: params["version"] = version - # TODO(SKY-7807): Replace with skyvern.get_workflow() when the Fern client adds it. + # SKY-7807: Replace with skyvern.get_workflow() when the Fern client adds it. response = await skyvern._client_wrapper.httpx_client.request( f"api/v1/workflows/{workflow_id}", method="GET", @@ -420,18 +448,145 @@ async def _get_workflow_by_id(workflow_id: str, version: int | None = None) -> d return response.json() -def _validate_definition_structure(json_def: WorkflowCreateYamlRequest | None, action: str) -> dict[str, Any] | None: +# --------------------------------------------------------------------------- +# Fern-bypass raw HTTP helpers +# +# The vendored Fern SDK ``skyvern/client`` validates Workflow requests and +# responses through discriminated unions of block variants; any block_type that +# the client hasn't been regenerated for (e.g. google_sheets_read) blows up the +# MCP tool before the backend can accept it. These helpers call the backend +# directly via the underlying httpx client, pass/return plain dicts, and +# sidestep the drift entirely. +# +# The ``v1/workflows`` paths intentionally mirror the generated Fern raw client. +# The ``api/v1`` workflow routes above are non-Fern/internal routes used only +# where no public SDK equivalent exists yet. +# --------------------------------------------------------------------------- + + +def _extract_error_detail(response: Any) -> str: + try: + body = response.json() + except Exception: + text = str(getattr(response, "text", "")) + return text[:_ERROR_DETAIL_LIMIT] if len(text) > _ERROR_DETAIL_LIMIT else text + if isinstance(body, dict): + return str(body.get("detail") or body.get("error") or body) + return str(body) + + +async def _list_workflows_raw( + *, + search: str | None, + page: int, + page_size: int, + only_workflows: bool, +) -> list[dict[str, Any]]: + skyvern = get_skyvern() + params: dict[str, Any] = { + "page": page, + "page_size": page_size, + "only_workflows": only_workflows, + } + if search is not None: + params["search_key"] = search + response = await skyvern._client_wrapper.httpx_client.request( + "v1/workflows", + method="GET", + params=params, + ) + if response.status_code >= 400: + raise RuntimeError(f"HTTP {response.status_code}: {_extract_error_detail(response)}") + payload = response.json() + if not isinstance(payload, list): + raise RuntimeError(f"Unexpected workflows list payload: {type(payload).__name__}") + return payload + + +async def _create_workflow_raw( + *, + json_definition: dict[str, Any] | None, + yaml_definition: str | None, + folder_id: str | None, +) -> dict[str, Any]: + skyvern = get_skyvern() + body: dict[str, Any] = {} + if json_definition is not None: + body["json_definition"] = json_definition + if yaml_definition is not None: + body["yaml_definition"] = yaml_definition + params: dict[str, Any] = {} + if folder_id is not None: + params["folder_id"] = folder_id + response = await skyvern._client_wrapper.httpx_client.request( + "v1/workflows", + method="POST", + params=params, + json=body, + ) + if response.status_code >= 400: + raise RuntimeError(f"HTTP {response.status_code}: {_extract_error_detail(response)}") + payload = response.json() + if not isinstance(payload, dict): + raise RuntimeError(f"Unexpected create_workflow payload: {type(payload).__name__}") + return payload + + +async def _update_workflow_raw( + workflow_id: str, + *, + json_definition: dict[str, Any] | None, + yaml_definition: str | None, +) -> dict[str, Any]: + skyvern = get_skyvern() + body: dict[str, Any] = {} + if json_definition is not None: + body["json_definition"] = json_definition + if yaml_definition is not None: + body["yaml_definition"] = yaml_definition + response = await skyvern._client_wrapper.httpx_client.request( + f"v1/workflows/{workflow_id}", + method="POST", + json=body, + ) + if response.status_code == 404: + raise NotFoundError(body={"detail": f"Workflow {workflow_id!r} not found"}) + if response.status_code >= 400: + raise RuntimeError(f"HTTP {response.status_code}: {_extract_error_detail(response)}") + payload = response.json() + if not isinstance(payload, dict): + raise RuntimeError(f"Unexpected update_workflow payload: {type(payload).__name__}") + return payload + + +async def _update_workflow_folder_raw(workflow_id: str, *, folder_id: str | None) -> dict[str, Any]: + skyvern = get_skyvern() + response = await skyvern._client_wrapper.httpx_client.request( + f"v1/workflows/{workflow_id}/folder", + method="PUT", + json={"folder_id": folder_id}, + ) + if response.status_code == 404: + raise NotFoundError(body={"detail": f"Workflow {workflow_id!r} not found"}) + if response.status_code == 400: + raise BadRequestError(body={"detail": _extract_error_detail(response)}) + if response.status_code >= 400: + raise RuntimeError(f"HTTP {response.status_code}: {_extract_error_detail(response)}") + payload = response.json() + if not isinstance(payload, dict): + raise RuntimeError(f"Unexpected update_workflow_folder payload: {type(payload).__name__}") + return payload + + +def _validate_definition_structure(json_def: dict[str, Any] | None, action: str) -> dict[str, Any] | None: """Validate required fields in a JSON workflow definition. Returns a make_result error dict if validation fails, or None if valid. Only validates JSON definitions — YAML is validated server-side. - Note: WorkflowCreateYamlRequest already enforces ``title`` and - ``workflow_definition`` as required fields via Pydantic, so this is - a belt-and-suspenders check that produces user-friendly error messages. """ if json_def is None: return None - if not json_def.title: + if not json_def.get("title"): return make_result( action, ok=False, @@ -441,6 +596,16 @@ def _validate_definition_structure(json_def: WorkflowCreateYamlRequest | None, a "Add a 'title' field to your workflow definition", ), ) + if not isinstance(json_def.get("workflow_definition"), dict): + return make_result( + action, + ok=False, + error=make_error( + ErrorCode.INVALID_INPUT, + "Workflow definition missing 'workflow_definition' object", + "Add a 'workflow_definition' object with a 'blocks' list", + ), + ) return None @@ -482,22 +647,33 @@ def _deep_merge(base: Any, override: Any) -> Any: return override -def _normalize_json_definition(raw: Any) -> WorkflowCreateYamlRequest: - """Normalize JSON workflow definitions through the shared backend schema.""" +def _normalize_json_definition(raw: Any) -> dict[str, Any]: + """Normalize JSON workflow definitions through the shared backend schema. + + The MCP tools post through raw HTTP so valid backend payloads are not + rejected by stale Fern request unions. When the backend schema accepts the + payload, merge its JSON-compatible normalization over the raw dict; when it + does not, preserve the caller's raw dict for server-side validation. + """ if not isinstance(raw, dict): raise TypeError("Workflow definition JSON must be an object") + if "title" not in raw: + raise ValueError("Workflow definition missing 'title' field") + if "workflow_definition" not in raw: + raise ValueError("Workflow definition missing 'workflow_definition' object") try: normalized = WorkflowCreateYAMLRequestSchema.model_validate(raw) except Exception as exc: - # Internal schema is stricter than the Fern SDK — skip normalization so - # unknown/future fields are not rejected. + # Internal schema is stricter than the API boundary — skip normalization + # so unknown/future fields are not rejected by MCP before the backend can + # decide. LOG.warning("Skipping text-prompt normalization; internal schema rejected payload", error=str(exc)) - return WorkflowCreateYamlRequest(**raw) + return raw merged = _deep_merge(raw, normalized.model_dump(mode="json")) - return WorkflowCreateYamlRequest(**merged) + return merged def _make_invalid_json_definition_error(exc: Exception) -> dict[str, Any]: @@ -792,14 +968,12 @@ async def _inject_workflow_update_parameters(definition: str, fmt: str, workflow return _dump_definition_dict(raw, parsed_format) -def _parse_definition( - definition: str, fmt: str -) -> tuple[WorkflowCreateYamlRequest | None, str | None, dict[str, Any] | None]: +def _parse_definition(definition: str, fmt: str) -> tuple[dict[str, Any] | None, str | None, dict[str, Any] | None]: """Parse a workflow definition string. Returns (json_definition, yaml_definition, error). Exactly one of the first two will be set on success, or error on failure. - JSON input is parsed into a WorkflowCreateYamlRequest (the type the SDK expects). + JSON input is parsed into a plain dict for raw HTTP submission. """ if fmt == "json": @@ -838,12 +1012,10 @@ async def skyvern_workflow_list( ) -> dict[str, Any]: """Find and browse available Skyvern workflows. Use when you need to discover what workflows exist, search for a workflow by name, or list all workflows for an organization.""" - skyvern = get_skyvern() - with Timer() as timer: try: - workflows = await skyvern.get_workflows( - search_key=search, + workflows = await _list_workflows_raw( + search=search, page=page, page_size=page_size, only_workflows=only_workflows, @@ -955,11 +1127,9 @@ async def skyvern_workflow_create( if err := _validate_definition_structure(json_def, "skyvern_workflow_create"): return err - skyvern = get_skyvern() - with Timer() as timer: try: - workflow = await skyvern.create_workflow( + workflow = await _create_workflow_raw( json_definition=json_def, yaml_definition=yaml_def, folder_id=folder_id, @@ -978,7 +1148,7 @@ async def skyvern_workflow_create( ), ) - LOG.info("workflow_created", workflow_id=workflow.workflow_permanent_id) + LOG.info("workflow_created", workflow_id=workflow.get("workflow_permanent_id")) data = _serialize_workflow(workflow) fmt_label = "json_definition" if json_def is not None else "yaml_definition" folder_str = f", folder_id={folder_id!r}" if folder_id is not None else "" @@ -1041,16 +1211,25 @@ async def skyvern_workflow_update( if err := _validate_definition_structure(json_def, "skyvern_workflow_update"): return err - skyvern = get_skyvern() - with Timer() as timer: try: - workflow = await skyvern.update_workflow( + workflow = await _update_workflow_raw( workflow_id, json_definition=json_def, yaml_definition=yaml_def, ) timer.mark("sdk") + except NotFoundError: + return make_result( + "skyvern_workflow_update", + ok=False, + timing_ms=timer.timing_ms, + error=make_error( + ErrorCode.WORKFLOW_NOT_FOUND, + f"Workflow {workflow_id!r} not found", + "Verify the workflow ID with skyvern_workflow_list", + ), + ) except Exception as e: return make_result( "skyvern_workflow_update", @@ -1140,11 +1319,9 @@ async def skyvern_workflow_update_folder( if folder_id is not None and (err := validate_folder_id(folder_id, "skyvern_workflow_update_folder")): return err - skyvern = get_skyvern() - with Timer() as timer: try: - workflow = await skyvern.update_workflow_folder(workflow_id, folder_id=folder_id) + workflow = await _update_workflow_folder_raw(workflow_id, folder_id=folder_id) timer.mark("sdk") except NotFoundError: return make_result( diff --git a/skyvern/constants.py b/skyvern/constants.py index fec50df09..837f25528 100644 --- a/skyvern/constants.py +++ b/skyvern/constants.py @@ -75,6 +75,17 @@ DEFAULT_LOGIN_COMPLETE_CRITERION = ( "Do NOT assume login failed just because you are on the homepage or the same page as before." ) +# Template for wrapping a block-level mini-goal with the user's original prompt as context. +# Used by both TaskV2 planning and the workflow-copilot-v2 tool handler so that every block's +# navigation_goal carries the user's overarching intent — the verifier (complete_verify) can +# then reason about completion against the user's goal rather than the block's narrow action +# decomposition. +MINI_GOAL_TEMPLATE = """Achieve the following mini goal and once it's achieved, complete: +```{mini_goal}``` + +This mini goal is part of the big goal the user wants to achieve and use the big goal as context to achieve the mini goal: +```{main_goal}```""" + # reserved fields for navigation payload SPECIAL_FIELD_VERIFICATION_CODE = "verification_code" diff --git a/skyvern/forge/agent.py b/skyvern/forge/agent.py index 4a00b2c09..827089490 100644 --- a/skyvern/forge/agent.py +++ b/skyvern/forge/agent.py @@ -135,6 +135,7 @@ from skyvern.webeye.actions.actions import ( KeypressAction, ReloadPageAction, TerminateAction, + VerificationStatus, WebAction, ) from skyvern.webeye.actions.handler import ActionHandler @@ -180,6 +181,66 @@ def _llm_error_category(reasoning: str) -> list[dict]: return [{"category": "LLM_ERROR", "confidence_float": 0.9, "reasoning": reasoning}] +# Phrases the verifier / validator LLMs use when they rely on exact-string +# matching rather than semantic interpretation. Tagging reasoning text as +# "literal" vs "semantic" gives us a post-hoc signal for how often the LLM +# narrow-matches on a criterion or goal — queryable via the +# validation.reasoning_kind / verification.reasoning_kind span attributes. +# The heuristic is imperfect by design; its purpose is a rough trend +# indicator across the copilot-v2 cohort and other callers. +_LITERAL_REASONING_SIGNALS: tuple[str, ...] = ( + "exact", + "literal", + "verbatim", + "word-for-word", + "word for word", +) + + +def _classify_reasoning_kind(reasoning: str | None) -> str: + """Classify a verifier / validator LLM's reasoning text as ``"literal"`` + (relied on exact-string matching) or ``"semantic"``. + + Shared between ``record_validation_span_attrs`` (validation-block step + spans) and ``record_verification_span_attrs`` (``complete_verify`` spans). + The keyword heuristic is imperfect by design — its purpose is a rough + trend signal, not a truth oracle. + """ + text = (reasoning or "").lower() + return "literal" if any(s in text for s in _LITERAL_REASONING_SIGNALS) else "semantic" + + +def record_validation_span_attrs(span: Any, task: Task, actions: list[Action]) -> None: + """Attach ``validation.decision`` and ``validation.reasoning_kind`` to + ``span`` when ``task`` is a ``TaskType.validation`` step whose first parsed + action is a ``CompleteAction`` or ``TerminateAction``. + + Exposed as a module-level helper (rather than inlined at the call site) so + the logic is unit-testable without driving the full agent step. No-op on + non-validation tasks or non-decisive actions — callers don't need to guard. + """ + if task.task_type != TaskType.validation or not actions: + return + decision = actions[0] + if not isinstance(decision, (CompleteAction, TerminateAction)): + return + validation_decision = "complete" if isinstance(decision, CompleteAction) else "terminate" + span.set_attribute("validation.decision", validation_decision) + span.set_attribute("validation.reasoning_kind", _classify_reasoning_kind(decision.reasoning)) + + +def record_verification_span_attrs(span: Any, verification_thoughts: str | None) -> None: + """Attach ``verification.reasoning_kind`` to the current + ``skyvern.agent.complete_verify`` span based on the verifier LLM's + ``thoughts`` field. + + Complements ``verification.status`` / ``verification.template``, which + ``complete_verify`` already sets. Pure, module-level helper so the + classifier logic is unit-testable without driving the full verify path. + """ + span.set_attribute("verification.reasoning_kind", _classify_reasoning_kind(verification_thoughts)) + + @dataclass class SpeculativePlan: scraped_page: ScrapedPage @@ -1372,6 +1433,7 @@ class ForgeAgent: speculative_llm_metadata = None detailed_agent_step_output.actions = actions + record_validation_span_attrs(_step_span, task, actions) if len(actions) == 0: LOG.info( "No actions to execute, marking step as failed", @@ -2334,6 +2396,7 @@ class ForgeAgent: exc_info=True, ) + @traced(name="skyvern.agent.complete_verify") async def complete_verify( self, page: Page, scraped_page: ScrapedPage, task: Task, step: Step ) -> CompleteVerifyResult: @@ -2437,7 +2500,18 @@ class ForgeAgent: screenshots=scraped_page_refreshed.screenshots, prompt_name=prompt_name, ) - return CompleteVerifyResult.model_validate(verification_result) + result = CompleteVerifyResult.model_validate(verification_result) + if result.is_complete: + verification_status = VerificationStatus.complete + elif result.is_terminate: + verification_status = VerificationStatus.terminate + else: + verification_status = VerificationStatus.continue_step + span = otel_trace.get_current_span() + span.set_attribute("verification.status", verification_status.value) + span.set_attribute("verification.template", template_name) + record_verification_span_attrs(span, result.thoughts) + return result async def check_user_goal_complete( self, page: Page, scraped_page: ScrapedPage, task: Task, step: Step diff --git a/skyvern/forge/prompts/skyvern/workflow-copilot-agent.j2 b/skyvern/forge/prompts/skyvern/workflow-copilot-agent.j2 index 3c4c6bb38..7fd45dcd2 100644 --- a/skyvern/forge/prompts/skyvern/workflow-copilot-agent.j2 +++ b/skyvern/forge/prompts/skyvern/workflow-copilot-agent.j2 @@ -114,9 +114,13 @@ When the user reports a problem with workflow output (duplicates, missing data, IMPORTANT WORKFLOW RULES: * Always generate valid YAML that conforms to the Skyvern workflow schema. -* Use "navigation" blocks for visiting URLs AND performing actions (filling forms, clicking buttons) — NOT "task" or "goto_url" blocks. -* Use "extraction" blocks for extracting structured data. -* Use "login" blocks for authentication flows. +* NEVER emit "task" or "task_v2" blocks. They are not supported by the workflow copilot and will be rejected at schema lookup. Decompose the work into specific block types instead: + - "navigation" for page actions (filling forms, clicking buttons, multi-step flows). + - "extraction" for data extraction. + - "validation" for completion checks. + - "login" for authentication flows. + - "wait" for explicit pauses between blocks. + - "goto_url" only when you need to navigate to a URL without any subsequent actions (prefer "navigation" when both URL visit + actions are needed). * Use descriptive, unique labels for blocks (snake_case format). * Reference parameters using Jinja2 syntax: {% raw %}{{ parameters.param_key }}{% endraw %} * Inline literal values directly in block config when the value is non-sensitive, used once, and not meant to be a reusable runtime input. Example: hardcode a specific URL or a one-off search term in the block's navigation_goal or data_extraction_goal instead of creating a workflow parameter for it. diff --git a/skyvern/forge/prompts/skyvern/workflow_knowledge_base.txt b/skyvern/forge/prompts/skyvern/workflow_knowledge_base.txt index 4010033ef..807492eb8 100644 --- a/skyvern/forge/prompts/skyvern/workflow_knowledge_base.txt +++ b/skyvern/forge/prompts/skyvern/workflow_knowledge_base.txt @@ -100,7 +100,7 @@ Important Rules: ** NAVIGATION BLOCK (navigation) ** -Purpose: Take actions to achieve a task. This is the "Browser Task" block in the UI. +Purpose: Take actions on a page to achieve a focused goal — fill a form, click through a multi-step flow, prepare the page before an extraction. Structure: block_type: navigation @@ -200,6 +200,16 @@ blocks: navigation_goal: "Check the terms checkbox" max_retries: 1 +** TASK BLOCK (task) — NOT AVAILABLE IN WORKFLOW COPILOT ** + +DO NOT EMIT "task" blocks. They are not available in the workflow copilot and will be rejected at persistence. Use: +- "navigation" for page actions (filling forms, clicking, multi-step flows) +- "extraction" for data extraction (with data_extraction_goal + data_schema) +- "validation" for completion checks +- "login" for authentication +- "goto_url" for pure URL navigation +Legacy workflows that already contain "task" blocks continue to run outside the copilot; this ban applies only to copilot emission. + ** TASK V2 BLOCK (task_v2) — DEPRECATED ** DO NOT USE task_v2. Use "navigation" blocks instead (with navigation_goal). diff --git a/skyvern/forge/sdk/copilot/agent.py b/skyvern/forge/sdk/copilot/agent.py index f128925c0..aba347e5c 100644 --- a/skyvern/forge/sdk/copilot/agent.py +++ b/skyvern/forge/sdk/copilot/agent.py @@ -22,6 +22,7 @@ import yaml from pydantic import ValidationError from skyvern.forge.prompts import prompt_engine +from skyvern.forge.sdk.copilot.block_goal_wrapping import wrap_block_goals from skyvern.forge.sdk.copilot.context import AgentResult, CopilotContext, StructuredContext from skyvern.forge.sdk.copilot.output_utils import extract_final_text, parse_final_response from skyvern.forge.sdk.copilot.tracing_setup import _copilot_model_name, ensure_tracing_initialized, is_tracing_enabled @@ -168,6 +169,7 @@ def _build_exit_result(ctx: CopilotContext, user_response: str, global_llm_conte global_llm_context=global_llm_context, workflow_yaml=verified_yaml, workflow_was_persisted=ctx.workflow_persisted, + total_tokens=ctx.total_tokens_used, ) @@ -178,6 +180,7 @@ def _translate_to_agent_result( chat_request: WorkflowCopilotChatRequest, organization_id: str, ) -> AgentResult: + # Deferred tools.py imports here and below: tools.py -> routes.workflow_copilot -> this module (circular at import time). from skyvern.forge.sdk.copilot.tools import _process_workflow_yaml text = extract_final_text(result) @@ -198,6 +201,26 @@ def _translate_to_agent_result( LOG.warning("Agent used inline REPLACE_WORKFLOW instead of update_workflow tool") workflow_yaml = action_data.get("workflow_yaml", "") if workflow_yaml: + # REPLACE_WORKFLOW bypasses _update_workflow, so the post-emission + # reject has to run here too. Skip processing on detection; leave + # last_workflow / last_workflow_yaml at their pre-REPLACE values so + # the rejected YAML does not latch onto ctx. + from skyvern.forge.sdk.copilot.tools import ( + _banned_block_reject_message, + _detect_new_banned_blocks, + _record_banned_block_reject_span, + ) + + banned_items = _detect_new_banned_blocks(workflow_yaml, ctx.last_workflow_yaml) + if banned_items: + _record_banned_block_reject_span("replace_workflow_inline", banned_items) + user_response = f"{user_response}\n\n(Note: {_banned_block_reject_message(banned_items)})" + workflow_yaml = "" + if workflow_yaml: + if ctx.user_message: + workflow_yaml = wrap_block_goals(workflow_yaml, ctx.user_message) + else: + LOG.warning("REPLACE_WORKFLOW inline path missing ctx.user_message; skipping block-goal wrap") try: last_workflow = _process_workflow_yaml( workflow_id=chat_request.workflow_id, @@ -251,6 +274,7 @@ def _translate_to_agent_result( response_type=resp_type, workflow_yaml=last_workflow_yaml, workflow_was_persisted=ctx.workflow_persisted, + total_tokens=ctx.total_tokens_used, ) @@ -486,6 +510,7 @@ async def run_copilot_agent( global_llm_context=global_llm_context, workflow_yaml=None, workflow_was_persisted=ctx.workflow_persisted, + total_tokens=ctx.total_tokens_used, ) except Exception as e: LOG.error("Copilot agent error", error=str(e), exc_info=True) diff --git a/skyvern/forge/sdk/copilot/block_goal_wrapping.py b/skyvern/forge/sdk/copilot/block_goal_wrapping.py new file mode 100644 index 000000000..d365ed9fc --- /dev/null +++ b/skyvern/forge/sdk/copilot/block_goal_wrapping.py @@ -0,0 +1,98 @@ +"""Wrap copilot-v2-generated block intent fields (``navigation_goal``, +``complete_criterion``, ``terminate_criterion``) with the user's original +chat message as "big goal" context, mirroring the TaskV2 pattern that +applies ``MINI_GOAL_TEMPLATE`` at every mini-goal construction site. + +Without this wrap: + - The Skyvern verifier (``complete_verify``) has no user-intent context + when a navigation block finishes on a confirmation surface. + - The validation-block prompt (``decisive-criterion-validate.j2``) sees + only a terse criterion (often a verbatim slice of the user prompt) and + reads it as a literal string to match. +""" + +from __future__ import annotations + +from typing import Any + +import yaml + +from skyvern.constants import MINI_GOAL_TEMPLATE +from skyvern.utils.yaml_loader import safe_load_no_dates + +# Block fields whose value expresses the LLM's "mini goal" — what it should +# do or what it should check for. Wrapped in MINI_GOAL_TEMPLATE alongside +# the user's chat message so the downstream LLMs can reason about intent. +# navigation_goal is carried by Task, Action, Navigation, Login, and +# FileDownload blocks; complete_criterion / terminate_criterion by Validation, +# Navigation, and Login blocks. +_WRAPPABLE_FIELDS: tuple[str, ...] = ( + "navigation_goal", + "complete_criterion", + "terminate_criterion", +) + +# The template's constant prefix — everything before the ``{mini_goal}`` +# placeholder. Presence of this prefix in a wrapped field means it was +# wrapped on a prior invocation; used for idempotency so repeated tool +# calls don't stack wrappers. Deriving from the template (rather than a +# hard-coded substring) keeps idempotency intact if the template's wording +# changes. +_WRAPPED_PREFIX = MINI_GOAL_TEMPLATE.partition("{mini_goal}")[0] + + +def wrap_block_goals(workflow_yaml: str, user_message: str) -> str: + """Return ``workflow_yaml`` with each block's ``navigation_goal``, + ``complete_criterion``, and ``terminate_criterion`` wrapped via + :data:`skyvern.constants.MINI_GOAL_TEMPLATE`. + + Blocks whose fields are missing, empty, or already wrapped are left + untouched. Recurses into ``ForLoopBlockYAML.loop_blocks``. No-ops when + ``user_message`` is empty or the YAML is malformed (malformed input is + surfaced by the downstream ``_process_workflow_yaml`` call, same as + today). + """ + if not user_message: + return workflow_yaml + # Skip the parse+dump round-trip when the YAML can't contain any wrappable + # field. False positives (field name appearing inside a value) are harmless: + # we'd fall through to the full path and mutate nothing. + if not any(field in workflow_yaml for field in _WRAPPABLE_FIELDS): + return workflow_yaml + try: + parsed = safe_load_no_dates(workflow_yaml) + except yaml.YAMLError: + return workflow_yaml + if not isinstance(parsed, dict): + return workflow_yaml + definition = parsed.get("workflow_definition") + if not isinstance(definition, dict): + return workflow_yaml + blocks = definition.get("blocks") + if not isinstance(blocks, list): + return workflow_yaml + if not _wrap_blocks_in_place(blocks, user_message): + return workflow_yaml + # parse/mutate/dump: any YAML comments in workflow_yaml are stripped on re-serialize. + return yaml.safe_dump(parsed, sort_keys=False) + + +def _wrap_blocks_in_place(blocks: list[Any], user_message: str) -> bool: + """Recursively wrap every field in :data:`_WRAPPABLE_FIELDS` on every + block in ``blocks``; returns ``True`` if at least one field was mutated.""" + mutated = False + for block in blocks: + if not isinstance(block, dict): + continue + for field_name in _WRAPPABLE_FIELDS: + value = block.get(field_name) + if isinstance(value, str) and value and _WRAPPED_PREFIX not in value: + block[field_name] = MINI_GOAL_TEMPLATE.format( + mini_goal=value, + main_goal=user_message, + ) + mutated = True + loop_blocks = block.get("loop_blocks") + if isinstance(loop_blocks, list): + mutated = _wrap_blocks_in_place(loop_blocks, user_message) or mutated + return mutated diff --git a/skyvern/forge/sdk/copilot/context.py b/skyvern/forge/sdk/copilot/context.py index 4ddf0d615..ff5e7e012 100644 --- a/skyvern/forge/sdk/copilot/context.py +++ b/skyvern/forge/sdk/copilot/context.py @@ -4,13 +4,15 @@ from __future__ import annotations import re from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from pydantic import BaseModel, Field from skyvern.forge.sdk.copilot.runtime import AgentContext from skyvern.forge.sdk.workflow.models.workflow import Workflow +ResponseType = Literal["REPLY", "ASK_QUESTION", "REPLACE_WORKFLOW"] + if TYPE_CHECKING: from skyvern.forge.sdk.copilot.narration import NarratorState @@ -108,13 +110,18 @@ class AgentResult: user_response: str updated_workflow: Workflow | None global_llm_context: str | None - response_type: str = "REPLY" + response_type: ResponseType = "REPLY" workflow_yaml: str | None = None workflow_was_persisted: bool = False # Feasibility-gate fast-path sets this True so the route can null any # previously-persisted proposed_workflow. Regular in-loop ASK_QUESTION # responses leave it False, preserving in-progress drafts. clear_proposed_workflow: bool = False + # Actual API token usage accumulated across the agent run. None when no + # provider reported usage on the stream — distinguishes "no data" from + # "0 tokens" so eval cost grading can flag missing telemetry instead of + # silently passing as cheap. + total_tokens: int | None = None @dataclass @@ -150,6 +157,14 @@ class CopilotContext(AgentContext): consecutive_tool_tracker: list[str] = field(default_factory=list) tool_activity: list[dict[str, Any]] = field(default_factory=list) + # Token usage summed from raw_responses after each streamed run. None + # until the first response that carries a usage object — some providers + # (notably non-OpenAI streaming routes) omit usage entirely, and we want + # eval cost grading to see "no data" rather than "0 tokens". + total_tokens_used: int | None = None + input_tokens_used: int | None = None + output_tokens_used: int | None = None + # Workflow state last_workflow: Workflow | None = None last_workflow_yaml: str | None = None diff --git a/skyvern/forge/sdk/copilot/enforcement.py b/skyvern/forge/sdk/copilot/enforcement.py index 35ea62324..73dc0952f 100644 --- a/skyvern/forge/sdk/copilot/enforcement.py +++ b/skyvern/forge/sdk/copilot/enforcement.py @@ -918,6 +918,31 @@ class _SendTrackingStream: await self._inner.close() +def _accumulate_usage(result: RunResultStreaming, ctx: Any) -> None: + """Sum actual token usage from raw_responses onto the context. + + Called per enforcement iteration in a ``finally:`` so pre-overflow + response tokens are still counted even when ``stream_to_sse`` raises. + First observed usage flips the counters from ``None`` to ``0``; if no + response on this stream carries a usage object the counters stay + ``None``, which the eval surfaces as "telemetry missing" rather than + "ran for free". + """ + if not hasattr(ctx, "total_tokens_used"): + return + for resp in getattr(result, "raw_responses", []) or []: + usage = getattr(resp, "usage", None) + if usage is None: + continue + if ctx.total_tokens_used is None: + ctx.total_tokens_used = 0 + ctx.input_tokens_used = 0 + ctx.output_tokens_used = 0 + ctx.total_tokens_used += getattr(usage, "total_tokens", 0) or 0 + ctx.input_tokens_used += getattr(usage, "input_tokens", 0) or 0 + ctx.output_tokens_used += getattr(usage, "output_tokens", 0) or 0 + + async def run_with_enforcement( agent: Agent, initial_input: str | list, @@ -965,7 +990,10 @@ async def run_with_enforcement( ): try: result = Runner.run_streamed(agent, input=current_input, context=ctx, session=session, **runner_kwargs) - await stream_to_sse(result, tracked_stream, ctx) + try: + await stream_to_sse(result, tracked_stream, ctx) + finally: + _accumulate_usage(result, ctx) except Exception as e: if not _is_context_window_error(e): raise @@ -995,7 +1023,10 @@ async def run_with_enforcement( result = Runner.run_streamed( agent, input=current_input, context=ctx, session=session, **runner_kwargs ) - await stream_to_sse(result, tracked_stream, ctx) + try: + await stream_to_sse(result, tracked_stream, ctx) + finally: + _accumulate_usage(result, ctx) except Exception as retry_err: # Never retry twice; even a second overflow surfaces as a # real failure rather than spinning. diff --git a/skyvern/forge/sdk/copilot/tools.py b/skyvern/forge/sdk/copilot/tools.py index 7e095b795..9ed4429fc 100644 --- a/skyvern/forge/sdk/copilot/tools.py +++ b/skyvern/forge/sdk/copilot/tools.py @@ -20,6 +20,7 @@ from pydantic import ValidationError from skyvern.forge import app from skyvern.forge.failure_classifier import classify_from_failure_reason from skyvern.forge.sdk.artifact.models import ArtifactType +from skyvern.forge.sdk.copilot.block_goal_wrapping import wrap_block_goals from skyvern.forge.sdk.copilot.context import CopilotContext from skyvern.forge.sdk.copilot.failure_tracking import ( _canonical_block_config, @@ -45,6 +46,7 @@ from skyvern.forge.sdk.workflow.models.parameter import ( ) from skyvern.forge.sdk.workflow.models.workflow import Workflow, WorkflowRun, WorkflowRunStatus from skyvern.schemas.workflows import BlockType +from skyvern.utils.yaml_loader import safe_load_no_dates from skyvern.webeye.navigation import is_skip_inner_retry_error LOG = structlog.get_logger() @@ -360,7 +362,7 @@ async def _attach_failed_block_screenshots( task_id_to_block[task_id]["screenshot_b64"] = b64 -_BLOCK_RUNNING_TOOLS = frozenset({"run_blocks_and_collect_debug", "update_and_run_blocks"}) +BLOCK_RUNNING_TOOLS = frozenset({"run_blocks_and_collect_debug", "update_and_run_blocks"}) def _tool_loop_error(ctx: AgentContext, tool_name: str) -> str | None: @@ -376,7 +378,7 @@ def _tool_loop_error(ctx: AgentContext, tool_name: str) -> str | None: # detecting) and further attempts will just burn the tool timeout. Scoped # to block-running tools so planning/metadata tools (update_workflow, # list_credentials, get_run_results) stay unaffected. - if tool_name in _BLOCK_RUNNING_TOOLS: + if tool_name in BLOCK_RUNNING_TOOLS: # Reconciliation guard: the previous block-running tool call exited # without a trustworthy terminal status for its workflow run (the # watchdog's stagnation / ceiling / task_exit_unfinalized paths, or @@ -504,6 +506,14 @@ def _parameter_binding_invariant_error( async def _update_workflow(params: dict[str, Any], ctx: AgentContext) -> dict[str, Any]: workflow_yaml = params["workflow_yaml"] + # Post-emission reject of copilot-v2 writes that introduce a banned + # block type. The schema pre_hook only fires when the LLM consults the + # schema; this safety net fires regardless of emission path. Label-based + # diff preserves legacy workflows — only NEW banned labels trip the reject. + banned_items = _detect_new_banned_blocks(workflow_yaml, ctx.workflow_yaml) + if banned_items: + _record_banned_block_reject_span("_update_workflow", banned_items) + return {"ok": False, "error": _banned_block_reject_message(banned_items)} try: workflow = _process_workflow_yaml( workflow_id=ctx.workflow_id, @@ -1435,6 +1445,157 @@ async def _resolve_url_title(raw: dict[str, Any], ctx: AgentContext) -> tuple[st return url, title +# Block types the copilot must never emit. They delegate the entire goal to +# a separate agent, which bypasses copilot-level block decomposition and +# obfuscates issues the copilot should surface/handle directly. +_COPILOT_BANNED_BLOCK_TYPES: frozenset[str] = frozenset({"task", "task_v2"}) + +# Shared suffix across every LLM-facing rejection message for banned +# block emission — the pre-hook (schema-lookup reject) and the post- +# emission detector both steer the LLM toward the same alternatives. +_COPILOT_BANNED_BLOCK_ALTERNATIVES = ( + "Use `navigation` for page actions (filling forms, clicking, multi-step flows), " + "`extraction` for data extraction, `validation` for completion checks, " + "`login` for authentication, or `goto_url` for pure URL navigation." +) + + +def _banned_block_reject_message(items: list[tuple[str, str]]) -> str: + """Uniform error text for the post-emission reject, sharing the + alternatives suffix with the schema pre-hook.""" + labels = ", ".join(sorted({label for label, _ in items})) + types = sorted({block_type for _, block_type in items}) + types_part = " / ".join(repr(t) for t in types) + return ( + f"Block type {types_part} is not available in the workflow copilot. " + f"Offending labels: [{labels}]. " + f"{_COPILOT_BANNED_BLOCK_ALTERNATIVES}" + ) + + +def _record_banned_block_reject_span(source_tool: str, items: list[tuple[str, str]]) -> None: + """Emit the dedicated ``update_workflow_banned_block_reject`` span used + by post-rollout logfire trend queries.""" + with copilot_span( + "update_workflow_banned_block_reject", + data={ + "labels": [label for label, _ in items], + "block_types": sorted({block_type for _, block_type in items}), + "source_tool": source_tool, + }, + ): + pass + + +def _collect_banned_block_items(blocks: list[Any]) -> list[tuple[str, str]]: + """Recursively walk ``blocks`` (mirroring + :func:`skyvern.forge.sdk.copilot.block_goal_wrapping._wrap_blocks_in_place`) + and return ``(label, normalized_block_type)`` for every block whose type is + in :data:`_COPILOT_BANNED_BLOCK_TYPES`. Blocks missing ``label`` are + skipped — the downstream Pydantic validator surfaces those errors on its + own.""" + items: list[tuple[str, str]] = [] + for block in blocks: + if not isinstance(block, dict): + continue + raw_type = block.get("block_type") + if isinstance(raw_type, str): + normalized = raw_type.strip().lower() + if normalized in _COPILOT_BANNED_BLOCK_TYPES: + label = block.get("label") + if isinstance(label, str): + items.append((label, normalized)) + loop_blocks = block.get("loop_blocks") + if isinstance(loop_blocks, list): + items.extend(_collect_banned_block_items(loop_blocks)) + return items + + +def _parse_workflow_blocks(yaml_str: str | None) -> list[Any] | None: + """Parse ``yaml_str`` and return ``workflow_definition.blocks`` as a list, + or ``None`` if the YAML is missing, unparseable, or not in the expected + shape. Graceful on every failure so callers can treat ``None`` as 'nothing + to compare against.'""" + if not yaml_str: + return None + try: + parsed = safe_load_no_dates(yaml_str) + except yaml.YAMLError: + return None + if not isinstance(parsed, dict): + return None + definition = parsed.get("workflow_definition") + if not isinstance(definition, dict): + return None + blocks = definition.get("blocks") + return blocks if isinstance(blocks, list) else None + + +def _detect_new_banned_blocks( + submitted_yaml: str, + prior_workflow_yaml: str | None, +) -> list[tuple[str, str]]: + """Return ``[(label, block_type), ...]`` for every banned-type block in + ``submitted_yaml`` whose label is NOT present as a banned-type block in + ``prior_workflow_yaml``. Pure: no I/O, no logging. + + Recurses into ``for_loop.loop_blocks`` mirroring + :func:`skyvern.forge.sdk.copilot.block_goal_wrapping._wrap_blocks_in_place`. + Legacy workflows that carry ``task`` / ``task_v2`` blocks under unchanged + labels produce an empty list and therefore do not reject. + + Malformed YAML, missing ``workflow_definition``, or a non-list ``blocks`` + all produce an empty list — the downstream Pydantic validation in + ``_process_workflow_yaml`` surfaces the specific parse / shape error on + its own path. + """ + submitted_blocks = _parse_workflow_blocks(submitted_yaml) + if submitted_blocks is None: + return [] + submitted_items = _collect_banned_block_items(submitted_blocks) + if not submitted_items: + return [] + prior_blocks = _parse_workflow_blocks(prior_workflow_yaml) + prior_labels = {label for label, _ in _collect_banned_block_items(prior_blocks or [])} + return [(label, block_type) for label, block_type in submitted_items if label not in prior_labels] + + +async def _get_block_schema_pre_hook( + params: dict[str, Any], + ctx: AgentContext, +) -> dict[str, Any] | None: + """Short-circuit requests for banned block types with an explicit error. + Without this pre-hook the underlying MCP tool silently redirects ``task`` + and ``task_v2`` queries to ``navigation``'s schema, which makes the LLM + think the banned types are available.""" + block_type = params.get("block_type") + if not isinstance(block_type, str): + return None + normalized = block_type.strip().lower() + if normalized not in _COPILOT_BANNED_BLOCK_TYPES: + return None + return { + "ok": False, + "error": f"Block type {block_type!r} is not available in the workflow copilot. {_COPILOT_BANNED_BLOCK_ALTERNATIVES}", + } + + +async def _get_block_schema_post_hook( + result: dict[str, Any], + raw: dict[str, Any], + ctx: AgentContext, +) -> dict[str, Any]: + """Scrub banned block types from list-mode responses. Belt-and-suspenders + against future drift in ``BLOCK_SUMMARIES`` (which currently omits them).""" + data = result.get("data") + if isinstance(data, dict): + block_types = data.get("block_types") + if isinstance(block_types, dict): + for banned in _COPILOT_BANNED_BLOCK_TYPES: + block_types.pop(banned, None) + return result + + async def _evaluate_pre_hook( params: dict[str, Any], ctx: AgentContext, @@ -1621,7 +1782,10 @@ def get_skyvern_mcp_alias_map() -> dict[str, str]: def _build_skyvern_mcp_overlays() -> dict[str, SchemaOverlay]: return { - "get_block_schema": SchemaOverlay(), + "get_block_schema": SchemaOverlay( + pre_hook=_get_block_schema_pre_hook, + post_hook=_get_block_schema_post_hook, + ), "validate_block": SchemaOverlay(), "navigate_browser": SchemaOverlay( description=( @@ -2076,6 +2240,14 @@ async def update_and_run_blocks_tool( # the new one — we need the pre-update state to diff against. prior_definition = await _get_prior_workflow_definition(copilot_ctx) + # Wrap each block's navigation_goal / complete_criterion / terminate_criterion + # with the user's message as "big goal" context so downstream LLMs (verifier, + # validation-block prompt) have user-intent framing — mirrors TaskV2. + if copilot_ctx.user_message: + workflow_yaml = wrap_block_goals(workflow_yaml, copilot_ctx.user_message) + else: + LOG.warning("update_and_run_blocks invoked without copilot_ctx.user_message; skipping block-goal wrap") + # Step 1: Update the workflow with copilot_span("update_workflow", data={"yaml_length": len(workflow_yaml)}): update_result = await _update_workflow({"workflow_yaml": workflow_yaml}, copilot_ctx) diff --git a/skyvern/forge/sdk/core/skyvern_context.py b/skyvern/forge/sdk/core/skyvern_context.py index 1aa332b6e..feb6bad37 100644 --- a/skyvern/forge/sdk/core/skyvern_context.py +++ b/skyvern/forge/sdk/core/skyvern_context.py @@ -37,7 +37,6 @@ class SkyvernContext: max_screenshot_scrolls: int | None = None browser_container_ip: str | None = None browser_container_task_arn: str | None = None - is_remote_browser: bool = False feature_flag_entries: dict[str, bool | str | None] = field(default_factory=dict) # feature flags diff --git a/skyvern/forge/sdk/event/base.py b/skyvern/forge/sdk/event/base.py index e52834ed6..a7767ca2d 100644 --- a/skyvern/forge/sdk/event/base.py +++ b/skyvern/forge/sdk/event/base.py @@ -21,7 +21,7 @@ class CursorEventStrategy(ABC): pass @abstractmethod - async def click(self, page: Page, locator: Locator, timeout: float | None = None) -> None: + async def click(self, page: Page, locator: Locator) -> None: pass diff --git a/skyvern/forge/sdk/event/default.py b/skyvern/forge/sdk/event/default.py index a0d4e2705..5a3b5f007 100644 --- a/skyvern/forge/sdk/event/default.py +++ b/skyvern/forge/sdk/event/default.py @@ -34,12 +34,8 @@ class DefaultCursorStrategy(CursorEventStrategy): LOG.debug("move_to_element failed", exc_info=True) return 0.0, 0.0 - async def click(self, page: Page, locator: Locator, timeout: float | None = None) -> None: - LOG.debug("DefaultCursorStrategy.click", timeout=timeout) - kwargs: dict = {} - if timeout is not None: - kwargs["timeout"] = timeout - await locator.click(**kwargs) + async def click(self, page: Page, locator: Locator) -> None: + await locator.click() class DefaultInputStrategy(InputEventStrategy): diff --git a/skyvern/forge/sdk/event/factory.py b/skyvern/forge/sdk/event/factory.py index 83ab61988..2e38cecc5 100644 --- a/skyvern/forge/sdk/event/factory.py +++ b/skyvern/forge/sdk/event/factory.py @@ -123,15 +123,6 @@ class EventStrategyFactory: """Update cursor position without generating movement.""" EventStrategyFactory.get_cursor_strategy().sync_position(page, x, y) - @staticmethod - async def click(page: Page, locator: Locator, timeout: float | None = None) -> None: - """Click an element using the active cursor strategy.""" - start = time.perf_counter() - try: - await EventStrategyFactory.get_cursor_strategy().click(page, locator, timeout) - finally: - EventStrategyFactory.__metrics.record("click", time.perf_counter() - start) - # -- input convenience methods ---------------------------------------------- @staticmethod diff --git a/skyvern/forge/sdk/routes/workflow_copilot.py b/skyvern/forge/sdk/routes/workflow_copilot.py index b8d88db88..96500e739 100644 --- a/skyvern/forge/sdk/routes/workflow_copilot.py +++ b/skyvern/forge/sdk/routes/workflow_copilot.py @@ -910,6 +910,8 @@ async def _new_copilot_chat_post( message=user_response, updated_workflow=updated_workflow.model_dump(mode="json") if updated_workflow else None, response_time=assistant_message.created_at, + total_tokens=getattr(agent_result, "total_tokens", None), + response_type=getattr(agent_result, "response_type", "REPLY"), ) ) except HTTPException as exc: diff --git a/skyvern/forge/sdk/schemas/workflow_copilot.py b/skyvern/forge/sdk/schemas/workflow_copilot.py index 127fbdd3e..89d5a05fb 100644 --- a/skyvern/forge/sdk/schemas/workflow_copilot.py +++ b/skyvern/forge/sdk/schemas/workflow_copilot.py @@ -3,6 +3,8 @@ from enum import StrEnum from pydantic import BaseModel, ConfigDict, Field +from skyvern.forge.sdk.copilot.context import ResponseType + class WorkflowCopilotChat(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -86,6 +88,11 @@ class WorkflowCopilotStreamResponseUpdate(BaseModel): message: str = Field(..., description="The message sent to the user") updated_workflow: dict | None = Field(None, description="The updated workflow") response_time: datetime = Field(..., description="When the assistant message was created") + total_tokens: int | None = Field( + None, + description="Total tokens consumed by the agent during this turn; None when no provider reported usage", + ) + response_type: ResponseType = Field("REPLY", description="Agent response classification") class WorkflowCopilotStreamErrorUpdate(BaseModel): diff --git a/skyvern/library/skyvern_locator.py b/skyvern/library/skyvern_locator.py index fc8593288..8409ddd0f 100644 --- a/skyvern/library/skyvern_locator.py +++ b/skyvern/library/skyvern_locator.py @@ -2,8 +2,6 @@ from typing import Any, Pattern from playwright.async_api import Locator -from skyvern.forge.sdk.event.factory import EventStrategyFactory - class SkyvernLocator: """Locator for finding and interacting with elements on a page. @@ -18,12 +16,7 @@ class SkyvernLocator: # Action methods async def click(self, **kwargs: Any) -> None: """Click the element.""" - timeout = kwargs.pop("timeout", None) - if kwargs: - # Extra kwargs (e.g. position) — use raw Playwright click - await self._locator.click(timeout=timeout, **kwargs) - else: - await EventStrategyFactory.click(self._locator.page, self._locator, timeout=timeout) + await self._locator.click(**kwargs) async def fill(self, value: str, **kwargs: Any) -> None: """Fill an input element with text.""" diff --git a/skyvern/services/task_v2_service.py b/skyvern/services/task_v2_service.py index 48b4af586..01d0c3120 100644 --- a/skyvern/services/task_v2_service.py +++ b/skyvern/services/task_v2_service.py @@ -9,6 +9,7 @@ from opentelemetry import trace as otel_trace from sqlalchemy.exc import OperationalError from skyvern.config import settings +from skyvern.constants import MINI_GOAL_TEMPLATE from skyvern.exceptions import ( FailedToSendWebhook, TaskTerminationError, @@ -77,12 +78,6 @@ RANDOM_STRING_POOL = string.ascii_letters + string.digits # This limits how many times the LLM can plan and execute actions DEFAULT_MAX_ITERATIONS = 50 -MINI_GOAL_TEMPLATE = """Achieve the following mini goal and once it's achieved, complete: -```{mini_goal}``` - -This mini goal is part of the big goal the user wants to achieve and use the big goal as context to achieve the mini goal: -```{main_goal}```""" - def _generate_data_extraction_schema_for_loop(loop_values_key: str) -> dict: return { diff --git a/skyvern/webeye/actions/handler.py b/skyvern/webeye/actions/handler.py index 0e27ceb03..1ba7ed098 100644 --- a/skyvern/webeye/actions/handler.py +++ b/skyvern/webeye/actions/handler.py @@ -2124,7 +2124,7 @@ async def handle_select_option_action( try: await EventStrategyFactory.move_to_element(page, skyvern_element.get_locator()) - await EventStrategyFactory.click(page, skyvern_element.get_locator(), timeout=timeout) + await skyvern_element.get_locator().click(timeout=timeout) except Exception: LOG.info( "fail to open dropdown by clicking, try to press arrow down to open", @@ -2664,7 +2664,7 @@ async def chain_click( try: if not await skyvern_element.navigate_to_a_href(page=page): await EventStrategyFactory.move_to_element(page, locator) - await EventStrategyFactory.click(page, locator, timeout=timeout) + await locator.click(timeout=timeout) LOG.info("Chain click: main element click succeeded", action=action, locator=locator) return [ActionSuccess()] @@ -2680,7 +2680,7 @@ async def chain_click( locator=locator, ) if bound_element := await skyvern_element.find_label_for(dom=dom): - await EventStrategyFactory.click(page, bound_element.get_locator(), timeout=timeout) + await bound_element.get_locator().click(timeout=timeout) action_results.append(ActionSuccess()) return action_results except Exception as e: @@ -2698,7 +2698,7 @@ async def chain_click( if bound_element := await skyvern_element.find_element_in_label_children( dom=dom, element_type=InteractiveElement.INPUT ): - await EventStrategyFactory.click(page, bound_element.get_locator(), timeout=timeout) + await bound_element.get_locator().click(timeout=timeout) action_results.append(ActionSuccess()) return action_results except Exception as e: @@ -2716,8 +2716,6 @@ async def chain_click( ) if bound_locator := await skyvern_element.find_bound_label_by_attr_id(): # click on (0, 0) to avoid playwright clicking on the wrong element by accident - # Intentional positional click bypassing EventStrategyFactory — position - # anchoring at (0,0) avoids mis-targeting nested elements inside labels await bound_locator.click(timeout=timeout, position={"x": 0, "y": 0}) action_results.append(ActionSuccess()) return action_results @@ -2735,8 +2733,6 @@ async def chain_click( ) if bound_locator := await skyvern_element.find_bound_label_by_direct_parent(): # click on (0, 0) to avoid playwright clicking on the wrong element by accident - # Intentional positional click bypassing EventStrategyFactory — position - # anchoring at (0,0) avoids mis-targeting nested elements inside labels await bound_locator.click(timeout=timeout, position={"x": 0, "y": 0}) action_results.append(ActionSuccess()) return action_results @@ -2816,7 +2812,7 @@ async def chain_click( locator=locator, ) - await EventStrategyFactory.click(page, blocking_element.get_locator(), timeout=timeout) + await blocking_element.get_locator().click(timeout=timeout) action_results.append(ActionSuccess()) return action_results except Exception as e: @@ -2927,9 +2923,7 @@ async def choose_auto_completion_dropdown( input_value=text, ) try: - await EventStrategyFactory.click( - page, fast_path_locator, timeout=settings.BROWSER_ACTION_TIMEOUT_MS - ) + await fast_path_locator.click(timeout=settings.BROWSER_ACTION_TIMEOUT_MS) clear_input = False result.action_result = ActionSuccess() return result @@ -3261,9 +3255,7 @@ async def discover_and_select_from_full_dropdown( # Try click first to open the dropdown (most combobox components respond to click) try: - await EventStrategyFactory.click( - page, skyvern_element.get_locator(), timeout=settings.BROWSER_ACTION_TIMEOUT_MS - ) + await skyvern_element.get_locator().click(timeout=settings.BROWSER_ACTION_TIMEOUT_MS) except Exception: LOG.info( "Click failed in discover fallback, continuing to ArrowDown", @@ -3880,7 +3872,7 @@ async def select_from_dropdown( value=value, ) await EventStrategyFactory.move_to_element(page, locator) - await EventStrategyFactory.click(page, locator, timeout=timeout) + await locator.click(timeout=timeout) single_select_result.action_result = ActionSuccess() return single_select_result except Exception as e: @@ -3909,7 +3901,7 @@ async def select_from_dropdown_by_value( element_locator = await incremental_scraped.select_one_element_by_value(value=value) if element_locator is not None: - await EventStrategyFactory.click(page, element_locator, timeout=timeout) + await element_locator.click(timeout=timeout) return ActionSuccess() if dropdown_menu_element is None: @@ -3945,7 +3937,7 @@ async def select_from_dropdown_by_value( element_locator = await incre_scraped.select_one_element_by_value(value=value) if element_locator is not None: - await EventStrategyFactory.click(page, element_locator, timeout=timeout) + await element_locator.click(timeout=timeout) nonlocal selected selected = True return False @@ -4234,7 +4226,9 @@ async def normal_select( value: str | None = json_response.get("value") try: - await EventStrategyFactory.click(locator.page, locator, timeout=settings.BROWSER_ACTION_TIMEOUT_MS) + await locator.click( + timeout=settings.BROWSER_ACTION_TIMEOUT_MS, + ) except Exception as e: LOG.info( "Failed to click before select action", @@ -4792,7 +4786,7 @@ async def click_listbox_option( try: skyvern_element = await dom.get_skyvern_element_by_id(child["id"]) locator = skyvern_element.locator - await EventStrategyFactory.click(page, locator, timeout=1000) + await locator.click(timeout=1000) return True except Exception: diff --git a/skyvern/webeye/browser_factory.py b/skyvern/webeye/browser_factory.py index 15bafaedb..913f75c73 100644 --- a/skyvern/webeye/browser_factory.py +++ b/skyvern/webeye/browser_factory.py @@ -795,11 +795,6 @@ async def _connect_to_cdp_browser( LOG.info("Connecting browser CDP connection", remote_browser_url=remote_browser_url) browser = await playwright.chromium.connect_over_cdp(remote_browser_url) - # Mark as remote browser so strategies can adapt their dispatch method - ctx = current() - if ctx is not None: - ctx.is_remote_browser = True - if apply_download_behaviour: await _apply_download_behaviour(browser) diff --git a/skyvern/webeye/utils/dom.py b/skyvern/webeye/utils/dom.py index 86d53dfd1..6fdf7d8cf 100644 --- a/skyvern/webeye/utils/dom.py +++ b/skyvern/webeye/utils/dom.py @@ -851,7 +851,7 @@ class SkyvernElement: await EventStrategyFactory.move_to_element(page, self.get_locator()) try: - await EventStrategyFactory.click(page, self.get_locator(), timeout=timeout) + await self.get_locator().click(timeout=timeout) return except Exception: LOG.info("Failed to click by playwright", exc_info=True, element_id=self.get_id()) @@ -863,7 +863,7 @@ class SkyvernElement: blocking_element, _ = await self.find_blocking_element(dom=dom, incremental_page=incremental_page) if blocking_element: LOG.debug("Find the blocking element", element_id=blocking_element.get_id()) - await EventStrategyFactory.click(page, blocking_element.get_locator(), timeout=timeout) + await blocking_element.get_locator().click(timeout=timeout) return except Exception: LOG.info("Failed to click on the blocking element", exc_info=True, element_id=self.get_id()) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 2fc98eed2..5effb0103 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,7 +1,13 @@ +"""Shared pytest fixtures and setup for unit tests.""" + # -- begin speed up unit tests from unittest.mock import AsyncMock, MagicMock import pytest +from opentelemetry import trace as otel_trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from tests.unit.force_stub_app import start_forge_stub_app @@ -59,3 +65,36 @@ def make_mock_session(mock_model: MagicMock) -> AsyncMock: mock_session.refresh = AsyncMock() return mock_session + + +# -- shared OTEL span capture for tests that assert on span attributes -- +# +# OTEL's global TracerProvider can only be set once per process. We install a +# single TracerProvider + InMemorySpanExporter at session start; tests that +# need span capture depend on the `span_exporter` fixture and get a cleared +# exporter for each test. + +_SHARED_SPAN_EXPORTER: InMemorySpanExporter | None = None + + +def _install_span_exporter() -> InMemorySpanExporter: + global _SHARED_SPAN_EXPORTER + if _SHARED_SPAN_EXPORTER is None: + exporter = InMemorySpanExporter() + provider = otel_trace.get_tracer_provider() + if isinstance(provider, TracerProvider): + provider.add_span_processor(SimpleSpanProcessor(exporter)) + else: + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + otel_trace.set_tracer_provider(provider) + _SHARED_SPAN_EXPORTER = exporter + return _SHARED_SPAN_EXPORTER + + +@pytest.fixture +def span_exporter() -> InMemorySpanExporter: + exporter = _install_span_exporter() + exporter.clear() + yield exporter + exporter.clear() diff --git a/tests/unit/test_complete_verify_span.py b/tests/unit/test_complete_verify_span.py new file mode 100644 index 000000000..5503c4f90 --- /dev/null +++ b/tests/unit/test_complete_verify_span.py @@ -0,0 +1,171 @@ +"""Tests for the `skyvern.agent.complete_verify` OTEL span attributes (SKY-9174). + +The verification span carries two attributes that power the logfire signal used +to measure SKY-9174's acceptance criterion post-rollout: + +- ``verification.status``: ``"complete" | "terminate" | "continue"`` +- ``verification.template``: ``"check-user-goal" | "check-user-goal-with-termination"`` + +These tests assert the attributes are set correctly across the three result +shapes and under both prompt-template selections. No behavioral change is being +verified — just the observability plumbing. +""" + +from __future__ import annotations + +from datetime import UTC, datetime +from unittest.mock import AsyncMock +from zoneinfo import ZoneInfo + +import pytest +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +from skyvern.forge.agent import ForgeAgent +from skyvern.forge.sdk.core import skyvern_context +from skyvern.forge.sdk.core.skyvern_context import SkyvernContext +from skyvern.forge.sdk.models import StepStatus +from tests.unit.helpers import make_browser_state, make_organization, make_step, make_task + +COMPLETE_VERIFY_SPAN_NAME = "skyvern.agent.complete_verify" + + +def _span_by_name(spans: list, name: str): + return next((s for s in spans if s.name == name), None) + + +async def _call_complete_verify( + monkeypatch: pytest.MonkeyPatch, + *, + llm_response: dict, + use_termination_prompt: bool, +) -> None: + agent = ForgeAgent() + now = datetime.now(UTC) + organization = make_organization(now) + task = make_task(now, organization, navigation_goal="Submit the contact form") + step = make_step( + now, + task, + step_id="step-verify", + status=StepStatus.running, + order=0, + output=None, + ) + _, scraped_page, page = make_browser_state() + + scraped_page_refreshed = AsyncMock() + scraped_page_refreshed.screenshots = [b"image"] + scraped_page.refresh = AsyncMock(return_value=scraped_page_refreshed) + + monkeypatch.setattr( + "skyvern.forge.agent.service_utils.is_cua_task", + AsyncMock(return_value=False), + ) + + async def feature_flag_side_effect(flag_name: str, *_args, **_kwargs) -> bool: + if flag_name == "USE_TERMINATION_AWARE_COMPLETE_VERIFICATION": + return use_termination_prompt + return False + + monkeypatch.setattr( + "skyvern.forge.agent.app.EXPERIMENTATION_PROVIDER.is_feature_enabled_cached", + AsyncMock(side_effect=feature_flag_side_effect), + ) + + monkeypatch.setattr( + "skyvern.forge.agent.load_prompt_with_elements", + lambda **_kwargs: "rendered prompt", + ) + + llm_handler = AsyncMock(return_value=llm_response) + monkeypatch.setattr( + "skyvern.forge.agent.LLMAPIHandlerFactory.get_override_llm_api_handler", + lambda *_args, **_kwargs: llm_handler, + ) + + context = SkyvernContext( + task_id=task.task_id, + step_id=None, + organization_id=task.organization_id, + workflow_run_id=task.workflow_run_id, + tz_info=ZoneInfo("UTC"), + ) + skyvern_context.set(context) + try: + await agent.complete_verify(page=page, scraped_page=scraped_page, task=task, step=step) + finally: + skyvern_context.reset() + + +@pytest.mark.asyncio +async def test_span_attrs_for_complete_status_legacy_prompt( + monkeypatch: pytest.MonkeyPatch, span_exporter: InMemorySpanExporter +) -> None: + await _call_complete_verify( + monkeypatch, + llm_response={"user_goal_achieved": True, "thoughts": "done", "page_info": "ok"}, + use_termination_prompt=False, + ) + span = _span_by_name(span_exporter.get_finished_spans(), COMPLETE_VERIFY_SPAN_NAME) + assert span is not None + attrs = span.attributes or {} + assert attrs.get("verification.status") == "complete" + assert attrs.get("verification.template") == "check-user-goal" + + +@pytest.mark.asyncio +async def test_span_attrs_for_continue_status_legacy_prompt( + monkeypatch: pytest.MonkeyPatch, span_exporter: InMemorySpanExporter +) -> None: + await _call_complete_verify( + monkeypatch, + llm_response={"user_goal_achieved": False, "thoughts": "still loading", "page_info": "spinner"}, + use_termination_prompt=False, + ) + span = _span_by_name(span_exporter.get_finished_spans(), COMPLETE_VERIFY_SPAN_NAME) + assert span is not None + attrs = span.attributes or {} + assert attrs.get("verification.status") == "continue" + assert attrs.get("verification.template") == "check-user-goal" + + +@pytest.mark.asyncio +async def test_span_attrs_for_terminate_status_termination_aware_prompt( + monkeypatch: pytest.MonkeyPatch, span_exporter: InMemorySpanExporter +) -> None: + await _call_complete_verify( + monkeypatch, + llm_response={ + "status": "terminate", + "thoughts": "blocked by captcha", + "page_info": "cloudflare", + "failure_categories": [{"category": "ANTI_BOT_DETECTION", "confidence_float": 0.9, "reasoning": "cf"}], + }, + use_termination_prompt=True, + ) + span = _span_by_name(span_exporter.get_finished_spans(), COMPLETE_VERIFY_SPAN_NAME) + assert span is not None + attrs = span.attributes or {} + assert attrs.get("verification.status") == "terminate" + assert attrs.get("verification.template") == "check-user-goal-with-termination" + + +@pytest.mark.asyncio +async def test_span_attrs_for_complete_status_termination_aware_prompt( + monkeypatch: pytest.MonkeyPatch, span_exporter: InMemorySpanExporter +) -> None: + await _call_complete_verify( + monkeypatch, + llm_response={ + "status": "complete", + "thoughts": "thank-you page visible", + "page_info": "thank-you", + "failure_categories": [], + }, + use_termination_prompt=True, + ) + span = _span_by_name(span_exporter.get_finished_spans(), COMPLETE_VERIFY_SPAN_NAME) + assert span is not None + attrs = span.attributes or {} + assert attrs.get("verification.status") == "complete" + assert attrs.get("verification.template") == "check-user-goal-with-termination" diff --git a/tests/unit/test_copilot_agent_helpers.py b/tests/unit/test_copilot_agent_helpers.py index a50e8d611..2a8f9be98 100644 --- a/tests/unit/test_copilot_agent_helpers.py +++ b/tests/unit/test_copilot_agent_helpers.py @@ -315,6 +315,37 @@ class TestTranslateToAgentResultGating: assert "All done" not in agent_result.user_response assert agent_result.updated_workflow is None + def test_inline_replace_workflow_wraps_block_goals_with_user_message(self, monkeypatch) -> None: + # SKY-9174 parity: update_and_run_blocks_tool wraps block goals with + # the user's chat message as big-goal context. The REPLACE_WORKFLOW + # inline path must do the same, otherwise the untested yaml latches + # onto ctx without user-intent framing and any downstream block run + # hits the verifier-on-confirmation-surface bug this PR fixes. + from skyvern.forge.sdk.copilot import agent as agent_module + + captured: dict[str, str] = {} + + def fake_process(**kwargs): + captured["yaml"] = kwargs["workflow_yaml"] + return SimpleNamespace(name="new-wf") + + def fake_wrap(workflow_yaml: str, user_message: str) -> str: + return f"WRAPPED::{user_message}::{workflow_yaml}" + + monkeypatch.setattr("skyvern.forge.sdk.copilot.tools._process_workflow_yaml", fake_process) + monkeypatch.setattr("skyvern.forge.sdk.copilot.agent.wrap_block_goals", fake_wrap) + + ctx = _ctx(user_message="Submit a contact form on example.com.") + result = _fake_run_result( + {"type": "REPLACE_WORKFLOW", "user_response": "Here you go.", "workflow_yaml": "raw: yaml"} + ) + agent_module._translate_to_agent_result( + result, ctx, global_llm_context=None, chat_request=_chat_request(), organization_id="org-1" + ) + + assert captured["yaml"] == "WRAPPED::Submit a contact form on example.com.::raw: yaml" + assert ctx.last_workflow_yaml == "WRAPPED::Submit a contact form on example.com.::raw: yaml" + class TestCredentialRefusalReachesAgent: """Prove the SKY-9189 refusal rule is actually delivered to the agent. diff --git a/tests/unit/test_copilot_block_goal_wrapping.py b/tests/unit/test_copilot_block_goal_wrapping.py new file mode 100644 index 000000000..01d269518 --- /dev/null +++ b/tests/unit/test_copilot_block_goal_wrapping.py @@ -0,0 +1,261 @@ +"""Tests for the copilot v2 block-goal wrap helper (SKY-9174, Part B). + +Covers wrapping of ``navigation_goal``, ``complete_criterion``, and +``terminate_criterion`` via ``MINI_GOAL_TEMPLATE``. +""" + +from __future__ import annotations + +import textwrap + +import yaml + +from skyvern.constants import MINI_GOAL_TEMPLATE +from skyvern.forge.sdk.copilot.block_goal_wrapping import wrap_block_goals + +USER_MESSAGE = "Submit a contact form on example.com with my details." + + +def _yaml_with_blocks(*blocks: dict) -> str: + return yaml.safe_dump( + { + "title": "test workflow", + "workflow_definition": {"blocks": list(blocks)}, + }, + sort_keys=False, + ) + + +def _blocks_from(yaml_str: str) -> list[dict]: + return yaml.safe_load(yaml_str)["workflow_definition"]["blocks"] + + +def _wrapped(mini_goal: str) -> str: + return MINI_GOAL_TEMPLATE.format(mini_goal=mini_goal, main_goal=USER_MESSAGE) + + +def test_wraps_task_block_navigation_goal() -> None: + src = _yaml_with_blocks( + {"block_type": "task", "label": "fill_form", "navigation_goal": "Fill in name and email, click Send"} + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + blocks = _blocks_from(out) + assert blocks[0]["navigation_goal"] == _wrapped("Fill in name and email, click Send") + + +def test_wraps_navigation_action_login_and_file_download_block_types() -> None: + src = _yaml_with_blocks( + {"block_type": "navigation", "label": "a", "navigation_goal": "nav goal"}, + {"block_type": "action", "label": "b", "navigation_goal": "action goal"}, + {"block_type": "login", "label": "c", "navigation_goal": "login goal"}, + {"block_type": "file_download", "label": "d", "navigation_goal": "download goal"}, + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + blocks = _blocks_from(out) + assert blocks[0]["navigation_goal"] == _wrapped("nav goal") + assert blocks[1]["navigation_goal"] == _wrapped("action goal") + assert blocks[2]["navigation_goal"] == _wrapped("login goal") + assert blocks[3]["navigation_goal"] == _wrapped("download goal") + + +def test_wraps_complete_criterion_on_validation_navigation_and_login_blocks() -> None: + src = _yaml_with_blocks( + {"block_type": "validation", "label": "v", "complete_criterion": "Your message has been sent"}, + { + "block_type": "navigation", + "label": "n", + "navigation_goal": "submit form", + "complete_criterion": "confirmation page visible", + }, + { + "block_type": "login", + "label": "l", + "navigation_goal": "login", + "complete_criterion": "user dashboard visible", + }, + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + blocks = _blocks_from(out) + assert blocks[0]["complete_criterion"] == _wrapped("Your message has been sent") + assert blocks[1]["navigation_goal"] == _wrapped("submit form") + assert blocks[1]["complete_criterion"] == _wrapped("confirmation page visible") + assert blocks[2]["navigation_goal"] == _wrapped("login") + assert blocks[2]["complete_criterion"] == _wrapped("user dashboard visible") + + +def test_wraps_terminate_criterion() -> None: + src = _yaml_with_blocks( + { + "block_type": "validation", + "label": "v", + "complete_criterion": "order placed", + "terminate_criterion": "payment failed", + }, + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + block = _blocks_from(out)[0] + assert block["complete_criterion"] == _wrapped("order placed") + assert block["terminate_criterion"] == _wrapped("payment failed") + + +def test_leaves_blocks_without_wrappable_fields_untouched() -> None: + src = _yaml_with_blocks( + {"block_type": "extraction", "label": "extract", "data_extraction_goal": "get title"}, + {"block_type": "goto_url", "label": "go", "url": "https://example.com"}, + {"block_type": "task", "label": "empty_goal", "navigation_goal": "", "complete_criterion": ""}, + {"block_type": "validation", "label": "null_crit", "complete_criterion": None}, + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + blocks = _blocks_from(out) + assert blocks[0] == {"block_type": "extraction", "label": "extract", "data_extraction_goal": "get title"} + assert blocks[1] == {"block_type": "goto_url", "label": "go", "url": "https://example.com"} + assert blocks[2]["navigation_goal"] == "" + assert blocks[2]["complete_criterion"] == "" + assert blocks[3]["complete_criterion"] is None + + +def test_idempotent_on_already_wrapped_fields() -> None: + already_wrapped_goal = _wrapped("Fill in name and email, click Send") + already_wrapped_criterion = _wrapped("Your message has been sent") + src = _yaml_with_blocks( + { + "block_type": "task", + "label": "fill_form", + "navigation_goal": already_wrapped_goal, + "complete_criterion": already_wrapped_criterion, + } + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + block = _blocks_from(out)[0] + assert block["navigation_goal"] == already_wrapped_goal + assert block["complete_criterion"] == already_wrapped_criterion + + +def test_noop_on_empty_user_message() -> None: + src = _yaml_with_blocks( + { + "block_type": "task", + "label": "fill_form", + "navigation_goal": "Fill the form", + "complete_criterion": "Your message has been sent", + } + ) + + out = wrap_block_goals(src, "") + + assert out == src + + +def test_noop_when_no_block_mutations_needed() -> None: + src = _yaml_with_blocks( + {"block_type": "extraction", "label": "extract", "data_extraction_goal": "get title"}, + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + assert out == src + + +def test_recurses_into_for_loop_blocks() -> None: + src = yaml.safe_dump( + { + "title": "loop workflow", + "workflow_definition": { + "blocks": [ + { + "block_type": "for_loop", + "label": "loop", + "loop_over": {"parameter_key": "items"}, + "loop_blocks": [ + {"block_type": "task", "label": "inner_task", "navigation_goal": "Process each item"}, + { + "block_type": "validation", + "label": "inner_check", + "complete_criterion": "item processed", + }, + ], + } + ] + }, + }, + sort_keys=False, + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + parsed = yaml.safe_load(out) + loop_blocks = parsed["workflow_definition"]["blocks"][0]["loop_blocks"] + assert loop_blocks[0]["navigation_goal"] == _wrapped("Process each item") + assert loop_blocks[1]["complete_criterion"] == _wrapped("item processed") + + +def test_preserves_other_fields() -> None: + src = _yaml_with_blocks( + { + "block_type": "task", + "label": "fill_form", + "url": "https://example.com", + "title": "Fill form", + "navigation_goal": "Fill in fields", + "parameter_keys": ["name", "email"], + "complete_criterion": "Form submitted", + "max_retries": 2, + } + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + block = _blocks_from(out)[0] + assert block["url"] == "https://example.com" + assert block["title"] == "Fill form" + assert block["parameter_keys"] == ["name", "email"] + assert block["max_retries"] == 2 + assert block["navigation_goal"] == _wrapped("Fill in fields") + assert block["complete_criterion"] == _wrapped("Form submitted") + + +def test_returns_input_unchanged_on_malformed_yaml() -> None: + malformed = textwrap.dedent( + """ + title: bad + workflow_definition: + blocks: + - block_type: task + navigation_goal: "unclosed + """ + ).strip() + + out = wrap_block_goals(malformed, USER_MESSAGE) + + assert out == malformed + + +def test_returns_input_unchanged_when_workflow_definition_missing() -> None: + src = yaml.safe_dump({"title": "no definition"}, sort_keys=False) + + out = wrap_block_goals(src, USER_MESSAGE) + + assert out == src + + +def test_returns_input_unchanged_when_blocks_not_list() -> None: + src = yaml.safe_dump( + {"title": "bad blocks", "workflow_definition": {"blocks": "not a list"}}, + sort_keys=False, + ) + + out = wrap_block_goals(src, USER_MESSAGE) + + assert out == src diff --git a/tests/unit/test_copilot_cancel_helpers.py b/tests/unit/test_copilot_cancel_helpers.py index 8ae451c1d..c6554c263 100644 --- a/tests/unit/test_copilot_cancel_helpers.py +++ b/tests/unit/test_copilot_cancel_helpers.py @@ -238,7 +238,7 @@ def test_trusted_post_drain_handles_missing_row() -> None: from types import SimpleNamespace as _NS # noqa: E402 (grouped with test block) -from skyvern.forge.sdk.copilot.tools import _BLOCK_RUNNING_TOOLS, _tool_loop_error # noqa: E402 +from skyvern.forge.sdk.copilot.tools import BLOCK_RUNNING_TOOLS, _tool_loop_error # noqa: E402 def _guard_ctx(pending_run_id: str | None = None) -> _NS: @@ -251,9 +251,9 @@ def _guard_ctx(pending_run_id: str | None = None) -> _NS: ) -@pytest.mark.parametrize("tool_name", sorted(_BLOCK_RUNNING_TOOLS)) +@pytest.mark.parametrize("tool_name", sorted(BLOCK_RUNNING_TOOLS)) def test_reconciliation_guard_blocks_block_running_tools(tool_name: str) -> None: - """With a pending run set, every tool in ``_BLOCK_RUNNING_TOOLS`` is + """With a pending run set, every tool in ``BLOCK_RUNNING_TOOLS`` is rejected with an error that names the run id and directs the LLM to call ``get_run_results`` first.""" ctx = _guard_ctx(pending_run_id="wr_pending_123") @@ -268,7 +268,7 @@ def test_reconciliation_guard_blocks_block_running_tools(tool_name: str) -> None ["get_run_results", "update_workflow", "list_credentials"], ) def test_reconciliation_guard_ignores_non_block_running_tools(tool_name: str) -> None: - """The guard is scoped to ``_BLOCK_RUNNING_TOOLS``. Planning / metadata + """The guard is scoped to ``BLOCK_RUNNING_TOOLS``. Planning / metadata tools (including ``get_run_results`` itself, which is the tool that can CLEAR the flag) must not be rejected.""" ctx = _guard_ctx(pending_run_id="wr_pending_123") @@ -279,7 +279,7 @@ def test_reconciliation_guard_passes_when_flag_empty() -> None: """No pending run → `_tool_loop_error` returns None for block-running tools (assuming no other guard trips).""" ctx = _guard_ctx(pending_run_id=None) - for name in _BLOCK_RUNNING_TOOLS: + for name in BLOCK_RUNNING_TOOLS: assert _tool_loop_error(ctx, name) is None @@ -288,7 +288,7 @@ def test_reconciliation_guard_rejects_empty_string_run_id() -> None: not set. Prevents a spurious guard trip if anything ever clears the flag to ``''`` instead of ``None``.""" ctx = _guard_ctx(pending_run_id="") - for name in _BLOCK_RUNNING_TOOLS: + for name in BLOCK_RUNNING_TOOLS: assert _tool_loop_error(ctx, name) is None diff --git a/tests/unit/test_copilot_non_retriable_nav.py b/tests/unit/test_copilot_non_retriable_nav.py index d79b8e419..8bdf63952 100644 --- a/tests/unit/test_copilot_non_retriable_nav.py +++ b/tests/unit/test_copilot_non_retriable_nav.py @@ -570,7 +570,7 @@ def test_tool_loop_error_blocks_run_blocks_and_collect_debug_after_dns_failure() def test_tool_loop_error_does_not_block_planning_tools() -> None: # get_run_results / list_credentials / update_workflow are scoped out of - # _BLOCK_RUNNING_TOOLS and should remain callable so the agent can inspect + # BLOCK_RUNNING_TOOLS and should remain callable so the agent can inspect # the failure and decide how to respond to the user. ctx = _fresh_context() ctx.last_test_non_retriable_nav_error = _DNS_FAILURE_REASON diff --git a/tests/unit/test_copilot_schema_overlay_ban.py b/tests/unit/test_copilot_schema_overlay_ban.py new file mode 100644 index 000000000..a551b14b1 --- /dev/null +++ b/tests/unit/test_copilot_schema_overlay_ban.py @@ -0,0 +1,114 @@ +"""Tests for the copilot-v2 SchemaOverlay hooks that ban task/task_v2 block +types at the discovery surface (SKY-9174, Part C).""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from skyvern.forge.sdk.copilot.tools import ( + _COPILOT_BANNED_BLOCK_TYPES, + _get_block_schema_post_hook, + _get_block_schema_pre_hook, +) + + +@pytest.fixture +def ctx() -> MagicMock: + return MagicMock() + + +@pytest.mark.parametrize("block_type", ["task", "task_v2", "TASK", "Task_V2", " task "]) +@pytest.mark.asyncio +async def test_pre_hook_blocks_banned_types_case_and_whitespace_insensitive(block_type: str, ctx: MagicMock) -> None: + result = await _get_block_schema_pre_hook({"block_type": block_type}, ctx) + + assert result is not None + assert result["ok"] is False + assert "not available in the workflow copilot" in result["error"] + for alternative in ("navigation", "extraction", "validation", "login"): + assert alternative in result["error"] + + +@pytest.mark.asyncio +async def test_pre_hook_allows_non_banned_types(ctx: MagicMock) -> None: + for block_type in ("navigation", "extraction", "validation", "login", "goto_url", "for_loop"): + assert await _get_block_schema_pre_hook({"block_type": block_type}, ctx) is None + + +@pytest.mark.asyncio +async def test_pre_hook_allows_list_mode_no_block_type(ctx: MagicMock) -> None: + assert await _get_block_schema_pre_hook({}, ctx) is None + assert await _get_block_schema_pre_hook({"block_type": None}, ctx) is None + + +@pytest.mark.asyncio +async def test_pre_hook_allows_non_string_block_type(ctx: MagicMock) -> None: + assert await _get_block_schema_pre_hook({"block_type": 123}, ctx) is None + + +@pytest.mark.asyncio +async def test_post_hook_scrubs_banned_types_from_list_response(ctx: MagicMock) -> None: + result = { + "ok": True, + "data": { + "block_types": { + "navigation": "Take actions on a page", + "task": "deprecated", + "task_v2": "deprecated", + "extraction": "Extract data", + }, + "count": 4, + }, + } + + out = await _get_block_schema_post_hook(result, raw={}, ctx=ctx) + + assert set(out["data"]["block_types"]) == {"navigation", "extraction"} + + +@pytest.mark.asyncio +async def test_post_hook_passthrough_when_no_block_types_dict(ctx: MagicMock) -> None: + result = {"ok": True, "data": {"block_type": "navigation", "summary": "..."}} + + out = await _get_block_schema_post_hook(result, raw={}, ctx=ctx) + + assert out == {"ok": True, "data": {"block_type": "navigation", "summary": "..."}} + + +@pytest.mark.asyncio +async def test_post_hook_handles_missing_or_malformed_data(ctx: MagicMock) -> None: + assert await _get_block_schema_post_hook({"ok": False, "error": "x"}, raw={}, ctx=ctx) == { + "ok": False, + "error": "x", + } + assert await _get_block_schema_post_hook({"ok": True, "data": None}, raw={}, ctx=ctx) == { + "ok": True, + "data": None, + } + assert await _get_block_schema_post_hook( + {"ok": True, "data": {"block_types": ["not", "a", "dict"]}}, raw={}, ctx=ctx + ) == {"ok": True, "data": {"block_types": ["not", "a", "dict"]}} + + +def test_banned_types_set_contents() -> None: + assert _COPILOT_BANNED_BLOCK_TYPES == frozenset({"task", "task_v2"}) + + +def test_pre_hook_and_post_emission_reject_share_constant() -> None: + """SKY-9174 Part F: the pre-emission SchemaOverlay hooks and the + post-emission YAML-level reject (in `_update_workflow` + REPLACE_WORKFLOW) + both import `_COPILOT_BANNED_BLOCK_TYPES` from the same module. Guard + against a future refactor that redefines the set in only one place — + any divergence would leave one of the two layers out of sync.""" + import skyvern.forge.sdk.copilot.tools as tools_module + + # `_detect_new_banned_blocks` exists on the same module and is the + # post-emission counterpart. If either symbol is removed, the layer is + # effectively ripped out and we want this test to catch it. + assert hasattr(tools_module, "_COPILOT_BANNED_BLOCK_TYPES") + assert hasattr(tools_module, "_get_block_schema_pre_hook") + assert hasattr(tools_module, "_get_block_schema_post_hook") + assert hasattr(tools_module, "_detect_new_banned_blocks") + assert hasattr(tools_module, "_banned_block_reject_message") diff --git a/tests/unit/test_copilot_task_block_rejection.py b/tests/unit/test_copilot_task_block_rejection.py new file mode 100644 index 000000000..701741067 --- /dev/null +++ b/tests/unit/test_copilot_task_block_rejection.py @@ -0,0 +1,349 @@ +"""Tests for the copilot-v2 post-emission reject of ``task`` / ``task_v2`` block +types (SKY-9174, Part F). + +Part C.1 banned the types at the schema-lookup surface via `SchemaOverlay` +pre / post hooks, but the LLM can bypass that by writing YAML directly without +querying the schema. Part F closes the bypass with a YAML-level reject that +fires on every copilot-v2 write path (``_update_workflow`` + inline +``REPLACE_WORKFLOW``), keyed by block label so legacy workflows with +pre-existing ``task`` blocks can still be edited by the copilot. +""" + +from __future__ import annotations + +import textwrap +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +import yaml + +from skyvern.forge.sdk.copilot.tools import _detect_new_banned_blocks, _update_workflow + + +def _yaml(*blocks: dict) -> str: + return yaml.safe_dump( + {"title": "wf", "workflow_definition": {"blocks": list(blocks)}}, + sort_keys=False, + ) + + +# ---------- Flat shapes ---------- + + +def test_top_level_task_block_is_detected_on_first_authoring() -> None: + submitted = _yaml({"block_type": "task", "label": "fill_contact_form", "navigation_goal": "do thing"}) + result = _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) + assert result == [("fill_contact_form", "task")] + + +def test_top_level_task_v2_block_is_detected() -> None: + submitted = _yaml({"block_type": "task_v2", "label": "legacy_taskv2", "prompt": "do it"}) + result = _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) + assert result == [("legacy_taskv2", "task_v2")] + + +def test_case_and_whitespace_insensitive() -> None: + submitted = _yaml( + {"block_type": "TASK", "label": "a"}, + {"block_type": " task_v2 ", "label": "b"}, + {"block_type": "Task", "label": "c"}, + ) + result = _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) + assert sorted(result) == [("a", "task"), ("b", "task_v2"), ("c", "task")] + + +def test_mixed_task_and_navigation_only_reports_banned() -> None: + submitted = _yaml( + {"block_type": "navigation", "label": "nav_a", "navigation_goal": "ok"}, + {"block_type": "task", "label": "bad", "navigation_goal": "bad"}, + {"block_type": "extraction", "label": "ext_a"}, + ) + result = _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) + assert result == [("bad", "task")] + + +def test_only_allowed_types_returns_empty() -> None: + submitted = _yaml( + {"block_type": "navigation", "label": "n"}, + {"block_type": "extraction", "label": "e"}, + {"block_type": "validation", "label": "v"}, + {"block_type": "login", "label": "lg"}, + {"block_type": "goto_url", "label": "g"}, + ) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) == [] + + +# ---------- Malformed ---------- + + +def test_malformed_yaml_is_graceful_no_op() -> None: + # Intentional parse failure — missing close quote. + assert _detect_new_banned_blocks("title: 'unterminated", prior_workflow_yaml=None) == [] + + +def test_missing_workflow_definition_is_graceful_no_op() -> None: + assert _detect_new_banned_blocks("title: wf\n", prior_workflow_yaml=None) == [] + + +def test_blocks_key_not_a_list_is_graceful_no_op() -> None: + bad = "title: wf\nworkflow_definition:\n blocks: not-a-list\n" + assert _detect_new_banned_blocks(bad, prior_workflow_yaml=None) == [] + + +def test_block_entry_not_a_dict_is_skipped() -> None: + # A bare string where a block dict is expected — should be skipped, not crash. + weird = textwrap.dedent( + """\ + title: wf + workflow_definition: + blocks: + - "not a block" + - block_type: task + label: real_banned + """ + ) + assert _detect_new_banned_blocks(weird, prior_workflow_yaml=None) == [("real_banned", "task")] + + +# ---------- Legacy preservation (RISK-1) ---------- + + +def test_preserved_legacy_task_block_under_same_label_does_not_reject() -> None: + prior = _yaml({"block_type": "task", "label": "legacy_task", "navigation_goal": "old"}) + submitted = _yaml({"block_type": "task", "label": "legacy_task", "navigation_goal": "old edited"}) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=prior) == [] + + +def test_new_task_block_alongside_preserved_legacy_reports_only_the_new_one() -> None: + prior = _yaml({"block_type": "task", "label": "legacy_task"}) + submitted = _yaml( + {"block_type": "task", "label": "legacy_task"}, + {"block_type": "task", "label": "fill_contact_form"}, + ) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=prior) == [("fill_contact_form", "task")] + + +def test_renamed_legacy_task_block_is_treated_as_new() -> None: + """Edge case: copilot re-emits a legacy task block under a different label. + The detector has no way to know this is a rename, so it's reported as new. + Acceptable: the copilot can recover by re-using the prior label.""" + prior = _yaml({"block_type": "task", "label": "old_name"}) + submitted = _yaml({"block_type": "task", "label": "new_name"}) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=prior) == [("new_name", "task")] + + +def test_prior_contains_allowed_types_submitted_adds_task_rejects() -> None: + prior = _yaml({"block_type": "navigation", "label": "nav"}) + submitted = _yaml( + {"block_type": "navigation", "label": "nav"}, + {"block_type": "task", "label": "bad_new"}, + ) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=prior) == [("bad_new", "task")] + + +def test_legacy_task_v2_preservation() -> None: + prior = _yaml({"block_type": "task_v2", "label": "legacy_v2"}) + submitted = _yaml({"block_type": "task_v2", "label": "legacy_v2"}) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=prior) == [] + + +# ---------- Nested (COMP-1) ---------- + + +def test_task_block_inside_for_loop_is_detected() -> None: + submitted = _yaml( + { + "block_type": "for_loop", + "label": "loop", + "loop_blocks": [ + {"block_type": "navigation", "label": "inner_nav"}, + {"block_type": "task", "label": "inner_bad"}, + ], + } + ) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) == [("inner_bad", "task")] + + +def test_nested_preservation_does_not_reject() -> None: + prior = _yaml( + { + "block_type": "for_loop", + "label": "loop", + "loop_blocks": [{"block_type": "task", "label": "nested_legacy"}], + } + ) + submitted = _yaml( + { + "block_type": "for_loop", + "label": "loop", + "loop_blocks": [{"block_type": "task", "label": "nested_legacy"}], + } + ) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=prior) == [] + + +def test_nested_new_addition_is_detected() -> None: + prior = _yaml( + { + "block_type": "for_loop", + "label": "loop", + "loop_blocks": [{"block_type": "navigation", "label": "nav_inner"}], + } + ) + submitted = _yaml( + { + "block_type": "for_loop", + "label": "loop", + "loop_blocks": [ + {"block_type": "navigation", "label": "nav_inner"}, + {"block_type": "task", "label": "new_nested_bad"}, + ], + } + ) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=prior) == [("new_nested_bad", "task")] + + +def test_deeply_nested_for_loop_is_walked() -> None: + """for_loop nested inside another for_loop — recursion must reach the innermost level.""" + submitted = _yaml( + { + "block_type": "for_loop", + "label": "outer", + "loop_blocks": [ + { + "block_type": "for_loop", + "label": "inner", + "loop_blocks": [{"block_type": "task", "label": "deeply_nested_bad"}], + } + ], + } + ) + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) == [("deeply_nested_bad", "task")] + + +# ---------- Missing label — should not crash ---------- + + +def test_block_without_label_is_skipped() -> None: + """A banned block missing the ``label`` key can't be identified for + preservation matching; skip it rather than crash. The YAML validator + downstream will surface the missing-label error on its own.""" + submitted = _yaml({"block_type": "task", "navigation_goal": "no label"}) + # No label → not collectible; result is empty (downstream Pydantic reject + # will surface the malformed block). + assert _detect_new_banned_blocks(submitted, prior_workflow_yaml=None) == [] + + +# ---------- Integration-shape tests: _update_workflow end-to-end ---------- +# +# These exercise the reject path at the tool-helper boundary, confirming the +# detection + error tool-result shape + dedicated OTEL span. The success path +# (YAML with only allowed types, or with preserved legacy task labels) is also +# covered — we patch ``_process_workflow_yaml`` and the workflow-service write +# so the test does not need a DB. + + +def _ctx(prior_yaml: str | None = None) -> MagicMock: + ctx = MagicMock() + ctx.workflow_yaml = prior_yaml + ctx.workflow_id = "w_test" + ctx.workflow_permanent_id = "wpid_test" + ctx.organization_id = "o_test" + return ctx + + +@pytest.mark.asyncio +async def test_update_workflow_rejects_new_task_block_and_emits_span() -> None: + submitted = _yaml({"block_type": "task", "label": "fill_contact_form", "navigation_goal": "do"}) + ctx = _ctx(prior_yaml=None) + + with patch("skyvern.forge.sdk.copilot.tools._record_banned_block_reject_span") as mock_span: + result = await _update_workflow({"workflow_yaml": submitted}, ctx) + + assert result["ok"] is False + assert "not available in the workflow copilot" in result["error"] + assert "fill_contact_form" in result["error"] + for alternative in ("navigation", "extraction", "validation", "login"): + assert alternative in result["error"] + + # Dedicated span fired with source_tool + items for logfire trend analysis. + mock_span.assert_called_once_with("_update_workflow", [("fill_contact_form", "task")]) + + +@pytest.mark.asyncio +async def test_update_workflow_preserves_legacy_task_block_under_unchanged_label() -> None: + """Copilot edit of a legacy workflow that already carries a ``task`` block + must not fail the reject. The helper sees the task label in prior YAML + and treats its re-emission as legacy preservation, not a new addition.""" + prior = _yaml({"block_type": "task", "label": "legacy_task", "navigation_goal": "old"}) + # New YAML preserves the legacy task block AND adds an allowed-type block. + submitted = _yaml( + {"block_type": "task", "label": "legacy_task", "navigation_goal": "old"}, + {"block_type": "navigation", "label": "new_nav", "navigation_goal": "new"}, + ) + ctx = _ctx(prior_yaml=prior) + + fake_workflow = MagicMock() + fake_workflow.title = "t" + fake_workflow.description = "d" + fake_workflow.workflow_definition = MagicMock() + fake_workflow.proxy_location = None + fake_workflow.webhook_callback_url = None + fake_workflow.persist_browser_session = False + fake_workflow.model = None + fake_workflow.max_screenshot_scrolls = None + fake_workflow.extra_http_headers = None + fake_workflow.run_with = None + fake_workflow.ai_fallback = None + fake_workflow.cache_key = None + fake_workflow.run_sequentially = None + fake_workflow.sequential_key = None + + with ( + patch("skyvern.forge.sdk.copilot.tools._process_workflow_yaml", return_value=fake_workflow), + patch("skyvern.forge.sdk.copilot.tools.app") as mock_app, + ): + mock_app.WORKFLOW_SERVICE.update_workflow_definition = AsyncMock() + result = await _update_workflow({"workflow_yaml": submitted}, ctx) + + assert result["ok"] is True + # The new YAML was accepted and assigned to ctx as the current workflow state. + assert ctx.workflow_yaml == submitted + + +@pytest.mark.asyncio +async def test_update_workflow_allows_all_allowed_block_types() -> None: + """Baseline success path: only allowed block types, no prior — passes through.""" + submitted = _yaml( + {"block_type": "navigation", "label": "n", "navigation_goal": "x"}, + {"block_type": "validation", "label": "v", "complete_criterion": "c"}, + ) + ctx = _ctx(prior_yaml=None) + + fake_workflow = MagicMock() + for attr in ( + "title", + "description", + "workflow_definition", + "proxy_location", + "webhook_callback_url", + "persist_browser_session", + "model", + "max_screenshot_scrolls", + "extra_http_headers", + "run_with", + "ai_fallback", + "cache_key", + "run_sequentially", + "sequential_key", + ): + setattr(fake_workflow, attr, None) + + with ( + patch("skyvern.forge.sdk.copilot.tools._process_workflow_yaml", return_value=fake_workflow), + patch("skyvern.forge.sdk.copilot.tools.app") as mock_app, + ): + mock_app.WORKFLOW_SERVICE.update_workflow_definition = AsyncMock() + result = await _update_workflow({"workflow_yaml": submitted}, ctx) + + assert result["ok"] is True diff --git a/tests/unit/test_llm_handler_tracing.py b/tests/unit/test_llm_handler_tracing.py index 44bdc828c..06b21d244 100644 --- a/tests/unit/test_llm_handler_tracing.py +++ b/tests/unit/test_llm_handler_tracing.py @@ -5,15 +5,9 @@ These tests verify the LLM chokepoint span + SKY-8414 `skyvern/forge/sdk/api/llm/api_handler_factory.py`. They serve as regression coverage for the instrumentation. -Note: OTEL's global TracerProvider can only be set once per process. This -module installs a shared TracerProvider + InMemorySpanExporter on first use -via `_ensure_provider()`. Other test files that also call -`otel_trace.set_tracer_provider(...)` will clobber or be clobbered depending -on import order. If more test files need span capture, move the provider -setup to a session-scoped fixture in conftest.py. - -The tests use OTEL's `InMemorySpanExporter` — no OTEL backend, collector, or -network required. Fast and deterministic. +The `span_exporter` fixture lives in `tests/unit/conftest.py` so any test +module that needs span capture can depend on it without installing its own +TracerProvider (OTEL's global provider can only be set once per process). """ from __future__ import annotations @@ -21,9 +15,6 @@ from __future__ import annotations from unittest.mock import AsyncMock, MagicMock import pytest # type: ignore[import-not-found] -from opentelemetry import trace as otel_trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from skyvern.forge.sdk.api.llm import api_handler_factory @@ -38,31 +29,6 @@ LLM_SPAN_NAME = "skyvern.llm.request" LLM_EVENT_NAME = "llm.request.completed" -_SHARED_EXPORTER: InMemorySpanExporter | None = None - - -def _ensure_provider() -> InMemorySpanExporter: - """OTEL's global TracerProvider can only be set once per process. Install - a shared TracerProvider + InMemorySpanExporter on first use; subsequent - tests reuse it and just clear the buffer between runs.""" - global _SHARED_EXPORTER - if _SHARED_EXPORTER is None: - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - otel_trace.set_tracer_provider(provider) - _SHARED_EXPORTER = exporter - return _SHARED_EXPORTER - - -@pytest.fixture -def span_exporter() -> InMemorySpanExporter: - exporter = _ensure_provider() - exporter.clear() - yield exporter - exporter.clear() - - def _span_by_name(spans: list, name: str): return next((s for s in spans if s.name == name), None) diff --git a/tests/unit/test_mcp_block_type_parity.py b/tests/unit/test_mcp_block_type_parity.py new file mode 100644 index 000000000..52ba27a96 --- /dev/null +++ b/tests/unit/test_mcp_block_type_parity.py @@ -0,0 +1,109 @@ +"""Parity guard between the backend BlockType enum and the Fern SDK unions. + +The vendored Fern SDK at ``skyvern/client/`` is regenerated manually. When a +new block type is added to ``skyvern/schemas/workflows.BlockType`` without +regenerating the SDK, MCP read paths that still deserialize through the Fern +``Workflow`` type break (see ``test_mcp_workflow_list_drift``). Even though the +MCP tools now bypass Fern for Workflow reads, we still want an early warning +when a regeneration is overdue so downstream clients that *do* use the Fern +SDK directly stay in sync. + +This test introspects both Fern discriminated unions and diffs them against +the backend enum. Known-drifted values are tolerated via an allowlist with a +Linear tracking pointer to the regeneration task; any NEW drift fails CI. +""" + +from __future__ import annotations + +import typing + +import pytest + +from skyvern.client.types.workflow_definition_blocks_item import WorkflowDefinitionBlocksItem +from skyvern.client.types.workflow_definition_yaml_blocks_item import WorkflowDefinitionYamlBlocksItem +from skyvern.schemas.workflows import BlockType + +# Known drift: block types present in the backend but not yet regenerated into +# the vendored Fern SDK. Run `fern generate` (or the equivalent Skyvern SDK +# regen workflow) to resync, then remove the entry here. +# Tracked follow-up: SKY-9227 +# https://linear.app/skyvern/issue/SKY-9227/prevent-fern-sdk-drift-from-breaking-workflow-block-types +# Remove this allowlist after the Fern SDK has been regenerated to include +# these values and downstream `skyvern` PyPI + `@skyvern/client` npm packages +# are published. +_KNOWN_DRIFT_ALLOWLIST: frozenset[str] = frozenset( + { + "google_sheets_read", + "google_sheets_write", + } +) + + +def _fern_union_block_types(union_type: typing.Any) -> set[str]: + """Extract the ``block_type`` Literal value from every variant of a Fern Union.""" + values: set[str] = set() + for variant in typing.get_args(union_type): + annotation = variant.model_fields["block_type"].annotation + literal_args = typing.get_args(annotation) + if not literal_args: + pytest.fail(f"Fern variant {variant.__name__} has a non-Literal block_type annotation: {annotation!r}") + values.update(str(arg) for arg in literal_args) + return values + + +def test_backend_block_types_present_in_fern_unions() -> None: + """Every BlockType value must be known to both Fern unions (or allowlisted).""" + backend_values = {member.value for member in BlockType} + + fern_blocks = _fern_union_block_types(WorkflowDefinitionBlocksItem) + fern_yaml_blocks = _fern_union_block_types(WorkflowDefinitionYamlBlocksItem) + fern_known = fern_blocks & fern_yaml_blocks + + missing_in_fern = backend_values - fern_known - _KNOWN_DRIFT_ALLOWLIST + assert not missing_in_fern, ( + "Fern SDK drift detected: the backend `skyvern.schemas.workflows.BlockType` " + f"has value(s) {sorted(missing_in_fern)!r} that the Fern discriminated unions " + "(`WorkflowDefinitionBlocksItem` / `WorkflowDefinitionYamlBlocksItem`) do not know. " + "Run `fern generate` to resync the vendored SDK at skyvern/client/, or add the " + "value to _KNOWN_DRIFT_ALLOWLIST in this test if the drift is intentional " + "and tracked." + ) + + +def test_fern_union_variants_are_subset_of_backend_or_deprecated() -> None: + """Fern may legitimately retain retired types during a deprecation window. + + This direction is informational: if Fern references a block_type the backend + no longer emits, we note it but don't fail — clients using a new SDK against + an older backend is the usual deprecation trajectory. + """ + backend_values = {member.value for member in BlockType} + + fern_blocks = _fern_union_block_types(WorkflowDefinitionBlocksItem) + fern_yaml_blocks = _fern_union_block_types(WorkflowDefinitionYamlBlocksItem) + + extras = (fern_blocks | fern_yaml_blocks) - backend_values + # Assert parity but allow the allowlist to flex in either direction so a + # retired block type stays tolerated for one regen cycle. + unexpected = extras - _KNOWN_DRIFT_ALLOWLIST + assert not unexpected, ( + f"Fern unions reference block_type value(s) {sorted(unexpected)!r} that are " + "not in the backend BlockType enum. If this is a planned deprecation, add " + "the value to _KNOWN_DRIFT_ALLOWLIST; otherwise investigate." + ) + + +def test_allowlist_entries_are_actually_drifted() -> None: + """Keeps _KNOWN_DRIFT_ALLOWLIST honest: drop stale entries after Fern regen.""" + backend_values = {member.value for member in BlockType} + + fern_blocks = _fern_union_block_types(WorkflowDefinitionBlocksItem) + fern_yaml_blocks = _fern_union_block_types(WorkflowDefinitionYamlBlocksItem) + fern_known = fern_blocks & fern_yaml_blocks + + currently_drifted = (backend_values - fern_known) | (fern_blocks ^ fern_yaml_blocks) + stale_allowlist_entries = _KNOWN_DRIFT_ALLOWLIST - currently_drifted + assert not stale_allowlist_entries, ( + f"_KNOWN_DRIFT_ALLOWLIST contains stale entries {sorted(stale_allowlist_entries)!r} " + "that no longer drift. Remove them to keep the allowlist tight." + ) diff --git a/tests/unit/test_mcp_folder_tools.py b/tests/unit/test_mcp_folder_tools.py index cd4601aca..4b45ca45f 100644 --- a/tests/unit/test_mcp_folder_tools.py +++ b/tests/unit/test_mcp_folder_tools.py @@ -8,7 +8,6 @@ import pytest import skyvern.cli.mcp_tools.folder as folder_tools import skyvern.cli.mcp_tools.workflow as workflow_tools -from skyvern.client.errors import BadRequestError from skyvern.client.raw_client import AsyncRawSkyvern, RawSkyvern @@ -110,12 +109,28 @@ async def test_folder_delete_handles_non_dict_sdk_result(monkeypatch: pytest.Mon @pytest.mark.asyncio async def test_workflow_update_folder_calls_sdk(monkeypatch: pytest.MonkeyPatch) -> None: - fake_client = SimpleNamespace(update_workflow_folder=AsyncMock(return_value=_fake_workflow_response())) + payload = { + "workflow_permanent_id": "wpid_test", + "workflow_id": "wf_test", + "title": "Example Workflow", + "version": 1, + "status": "published", + "description": None, + "is_saved_task": False, + "folder_id": "fld_test", + "created_at": "2026-04-23T10:00:00+00:00", + "modified_at": "2026-04-23T10:00:00+00:00", + } + response = SimpleNamespace(status_code=200, json=lambda: payload, text="") + request_mock = AsyncMock(return_value=response) + fake_client = SimpleNamespace(_client_wrapper=SimpleNamespace(httpx_client=SimpleNamespace(request=request_mock))) monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) result = await workflow_tools.skyvern_workflow_update_folder("wpid_test", "fld_test") - fake_client.update_workflow_folder.assert_awaited_once_with("wpid_test", folder_id="fld_test") + request_mock.assert_awaited_once() + call_kwargs = request_mock.await_args.kwargs + assert call_kwargs["json"]["folder_id"] == "fld_test" assert result["ok"] is True assert result["data"]["workflow_permanent_id"] == "wpid_test" assert result["data"]["folder_id"] == "fld_test" @@ -132,9 +147,10 @@ async def test_workflow_update_folder_rejects_invalid_folder_id() -> None: @pytest.mark.asyncio async def test_workflow_update_folder_surfaces_bad_request(monkeypatch: pytest.MonkeyPatch) -> None: - fake_client = SimpleNamespace( - update_workflow_folder=AsyncMock(side_effect=BadRequestError(body={"detail": "Folder fld_missing not found"})) - ) + error_payload = {"detail": "Folder fld_missing not found"} + response = SimpleNamespace(status_code=400, json=lambda: error_payload, text="Folder fld_missing not found") + request_mock = AsyncMock(return_value=response) + fake_client = SimpleNamespace(_client_wrapper=SimpleNamespace(httpx_client=SimpleNamespace(request=request_mock))) monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) result = await workflow_tools.skyvern_workflow_update_folder("wpid_test", "fld_missing") diff --git a/tests/unit/test_mcp_script_caching_live.py b/tests/unit/test_mcp_script_caching_live.py index 4e58c85e5..8aae3c09a 100644 --- a/tests/unit/test_mcp_script_caching_live.py +++ b/tests/unit/test_mcp_script_caching_live.py @@ -182,7 +182,8 @@ async def test_get_script_code_via_mcp(monkeypatch): assert result.data["ok"] is True data = result.data["data"] assert "fill_form" in data["blocks"] - assert "page.fill" in data["blocks"]["fill_form"] + # Semgrep false positive: this checks a script code path, not a user-supplied URL. + assert "page.fill" in data["blocks"]["fill_form"] # nosemgrep: incomplete-url-substring-sanitization assert "@skyvern.workflow" in data["main_script"] @@ -342,25 +343,24 @@ async def test_deploy_script_via_mcp(monkeypatch): @pytest.mark.asyncio async def test_workflow_create_surfaces_caching_fields_via_mcp(monkeypatch): - from datetime import datetime, timezone - - now = datetime.now(timezone.utc) - fake_wf = SimpleNamespace( - workflow_permanent_id="wpid_new", - workflow_id="wf_1", - title="Test", - version=1, - status="published", - description=None, - is_saved_task=False, - folder_id=None, - created_at=now, - modified_at=now, - code_version=2, - adaptive_caching=True, - run_with="code", - ) - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=fake_wf)) + payload = { + "workflow_permanent_id": "wpid_new", + "workflow_id": "wf_1", + "title": "Test", + "version": 1, + "status": "published", + "description": None, + "is_saved_task": False, + "folder_id": None, + "created_at": "2026-04-23T10:00:00+00:00", + "modified_at": "2026-04-23T10:00:00+00:00", + "code_version": 2, + "adaptive_caching": True, + "run_with": "code", + } + response = SimpleNamespace(status_code=200, json=lambda: payload, text="") + request_mock = AsyncMock(return_value=response) + fake_client = SimpleNamespace(_client_wrapper=SimpleNamespace(httpx_client=SimpleNamespace(request=request_mock))) monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) definition = json.dumps( diff --git a/tests/unit/test_mcp_workflow_create_defaults.py b/tests/unit/test_mcp_workflow_create_defaults.py index fa9b89f3c..370b0a4da 100644 --- a/tests/unit/test_mcp_workflow_create_defaults.py +++ b/tests/unit/test_mcp_workflow_create_defaults.py @@ -130,5 +130,7 @@ def test_parse_definition_unaffected() -> None: json_def, _, err = _parse_definition(definition, "json") assert err is None assert json_def is not None + assert isinstance(json_def, dict) # run_with should be "agent" (schema default), not "code" - assert json_def.run_with == "agent" + assert json_def.get("run_with") == "agent" + assert json_def.get("code_version") != 2 diff --git a/tests/unit/test_mcp_workflow_list_drift.py b/tests/unit/test_mcp_workflow_list_drift.py new file mode 100644 index 000000000..883ec5e1c --- /dev/null +++ b/tests/unit/test_mcp_workflow_list_drift.py @@ -0,0 +1,218 @@ +"""Regression tests for MCP workflow list resilience to Fern SDK drift. + +Context: the vendored Fern SDK at ``skyvern/client/`` validates workflow +responses through a discriminated pydantic Union that does not know about +block types added to the backend after the last SDK regeneration (currently +``google_sheets_read`` and ``google_sheets_write``). Before the Strategy B fix, +``skyvern_workflow_list`` deserialized through that stale Union and would +crash the entire page for any workflow whose definition referenced an +unknown block_type. These tests lock in the bypass: list calls go through +raw httpx and return plain dicts, so new backend block types pass through +unchanged. +""" + +from __future__ import annotations + +from types import SimpleNamespace +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import httpx +import pytest + +from skyvern.cli.mcp_tools import workflow as mcp_workflow +from skyvern.client.core.client_wrapper import AsyncClientWrapper + + +def _make_workflow_dict(workflow_id: str, block_type: str, *, label: str = "step1") -> dict[str, Any]: + """Minimal workflow payload shape, close enough to what ``v1/workflows`` returns.""" + block: dict[str, Any] = { + "block_type": block_type, + "label": label, + } + if block_type == "google_sheets_read": + block["spreadsheet_url"] = "https://docs.google.com/spreadsheets/d/xxx/edit" + block["sheet_name"] = "Sheet1" + block["range"] = "A1:D100" + block["credential_id"] = "{{ google_credential_id }}" + block["has_header_row"] = True + elif block_type == "navigation": + block["url"] = "https://example.com" + block["navigation_goal"] = "do the thing" + return { + "workflow_permanent_id": workflow_id, + "workflow_id": f"wf_{workflow_id.split('_', 1)[-1]}", + "title": f"Workflow {workflow_id}", + "version": 1, + "status": "published", + "description": None, + "is_saved_task": False, + "folder_id": None, + "created_at": "2026-04-20T10:00:00+00:00", + "modified_at": "2026-04-22T10:00:00+00:00", + "workflow_definition": { + "parameters": [], + "blocks": [block], + }, + } + + +def _patch_skyvern_list_response( + monkeypatch: pytest.MonkeyPatch, + *, + payload: list[dict[str, Any]], + status_code: int = 200, +) -> AsyncMock: + response = MagicMock() + response.status_code = status_code + response.json.return_value = payload + response.text = "" + + request_mock = AsyncMock(return_value=response) + httpx_client = SimpleNamespace(request=request_mock) + client_wrapper = SimpleNamespace(httpx_client=httpx_client) + fake_skyvern = SimpleNamespace(_client_wrapper=client_wrapper) + + monkeypatch.setattr(mcp_workflow, "get_skyvern", lambda: fake_skyvern) + return request_mock + + +@pytest.mark.asyncio +async def test_list_succeeds_when_workflow_uses_google_sheets_block(monkeypatch: pytest.MonkeyPatch) -> None: + """Regression: a google_sheets_read block must not break list deserialization. + + Pre-fix behavior: ``skyvern.get_workflows(...)`` ran ``parse_obj_as(List[Workflow], ...)`` + on the response, and the stale Fern discriminated Union raised ValidationError + on ``google_sheets_read``, taking the entire page down. The bypass returns raw + dicts, so any block_type string round-trips through the tool untouched. + """ + payload = [ + _make_workflow_dict("wpid_ok", "navigation"), + _make_workflow_dict("wpid_sheets", "google_sheets_read"), + ] + request_mock = _patch_skyvern_list_response(monkeypatch, payload=payload) + + result = await mcp_workflow.skyvern_workflow_list(page=1, page_size=10) + + assert result["ok"] is True, result + data = result["data"] + assert data["count"] == 2 + assert data["page"] == 1 + ids = {wf["workflow_permanent_id"] for wf in data["workflows"]} + assert ids == {"wpid_ok", "wpid_sheets"} + + request_mock.assert_awaited_once() + call = request_mock.await_args + assert call.args[0] == "v1/workflows" + assert call.kwargs["method"] == "GET" + params = call.kwargs["params"] + assert "search_key" not in params + assert params["page"] == 1 + assert params["page_size"] == 10 + assert params["only_workflows"] is False + + +@pytest.mark.asyncio +async def test_list_includes_search_key_only_when_search_is_present(monkeypatch: pytest.MonkeyPatch) -> None: + """Avoid leaking a literal search_key=None query parameter.""" + request_mock = _patch_skyvern_list_response(monkeypatch, payload=[]) + + result = await mcp_workflow.skyvern_workflow_list(search="invoice", page=2, page_size=5) + + assert result["ok"] is True, result + params = request_mock.await_args.kwargs["params"] + assert params == { + "search_key": "invoice", + "page": 2, + "page_size": 5, + "only_workflows": False, + } + + +@pytest.mark.asyncio +async def test_raw_list_uses_fern_http_wrapper_auth_headers(monkeypatch: pytest.MonkeyPatch) -> None: + """The raw bypass still uses Fern's HTTP wrapper, including auth and query encoding.""" + seen_requests: list[httpx.Request] = [] + + async def handler(request: httpx.Request) -> httpx.Response: + seen_requests.append(request) + return httpx.Response(200, json=[]) + + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as base_client: + client_wrapper = AsyncClientWrapper( + api_key="sk_test", + headers={"x-test-header": "present"}, + base_url="https://api.example.test/api", + timeout=60, + httpx_client=base_client, + ) + fake_skyvern = SimpleNamespace(_client_wrapper=client_wrapper) + monkeypatch.setattr(mcp_workflow, "get_skyvern", lambda: fake_skyvern) + + result = await mcp_workflow.skyvern_workflow_list(only_workflows=True) + + assert result["ok"] is True, result + assert len(seen_requests) == 1 + request = seen_requests[0] + assert request.url.path == "/api/v1/workflows" + assert request.headers["x-api-key"] == "sk_test" + assert request.headers["x-test-header"] == "present" + assert request.headers["x-fern-sdk-name"] == "skyvern" + assert request.url.params["only_workflows"] == "true" + + +def test_extract_error_detail_truncates_non_json_response_text() -> None: + """Avoid returning an entire proxy/load-balancer HTML page in MCP errors.""" + response = MagicMock() + response.json.side_effect = ValueError("not json") + response.text = "x" * 600 + + detail = mcp_workflow._extract_error_detail(response) + + assert detail == "x" * 500 + + +@pytest.mark.asyncio +async def test_list_surfaces_http_error(monkeypatch: pytest.MonkeyPatch) -> None: + """Non-2xx responses return an API_ERROR result, not a crash.""" + response = MagicMock() + response.status_code = 500 + response.json.return_value = {"detail": "boom"} + response.text = '{"detail": "boom"}' + + request_mock = AsyncMock(return_value=response) + fake_skyvern = SimpleNamespace(_client_wrapper=SimpleNamespace(httpx_client=SimpleNamespace(request=request_mock))) + monkeypatch.setattr(mcp_workflow, "get_skyvern", lambda: fake_skyvern) + + result = await mcp_workflow.skyvern_workflow_list() + + assert result["ok"] is False + assert "500" in result["error"]["message"] + + +@pytest.mark.asyncio +async def test_list_preserves_serialization_shape(monkeypatch: pytest.MonkeyPatch) -> None: + """Response payload keys stay stable so MCP clients see no contract change.""" + payload = [_make_workflow_dict("wpid_ok", "navigation")] + _patch_skyvern_list_response(monkeypatch, payload=payload) + + result = await mcp_workflow.skyvern_workflow_list(page_size=5) + + assert result["ok"] is True + data = result["data"] + assert set(data.keys()) == {"workflows", "page", "page_size", "count", "has_more", "sdk_equivalent"} + wf = data["workflows"][0] + expected_keys = { + "workflow_permanent_id", + "workflow_id", + "title", + "version", + "status", + "description", + "is_saved_task", + "folder_id", + "created_at", + "modified_at", + } + assert expected_keys <= set(wf.keys()) + assert wf["created_at"] == "2026-04-20T10:00:00+00:00" diff --git a/tests/unit/test_mcp_workflow_tools.py b/tests/unit/test_mcp_workflow_tools.py index c34eacd81..8f50e07bc 100644 --- a/tests/unit/test_mcp_workflow_tools.py +++ b/tests/unit/test_mcp_workflow_tools.py @@ -36,6 +36,73 @@ def _fake_http_response(payload: dict[str, object], status_code: int = 200) -> S ) +def _fake_workflow_dict(**overrides: object) -> dict[str, object]: + """Plain dict shape returned by `v1/workflows` — matches _fake_workflow_response().""" + data: dict[str, object] = { + "workflow_permanent_id": "wpid_test", + "workflow_id": "wf_test", + "title": "Example Workflow", + "version": 1, + "status": "published", + "description": None, + "is_saved_task": False, + "folder_id": None, + "created_at": "2026-04-23T10:00:00+00:00", + "modified_at": "2026-04-23T10:00:00+00:00", + } + data.update(overrides) + return data + + +def _patch_skyvern_http( + monkeypatch: pytest.MonkeyPatch, + *, + response_payload: object, + status_code: int = 200, +) -> AsyncMock: + """Patch ``workflow_tools.get_skyvern`` to return a fake client whose httpx + request returns the given payload. Returns the request AsyncMock so tests + can assert on what was sent. + """ + response = SimpleNamespace( + status_code=status_code, + json=lambda: response_payload, + text=json.dumps(response_payload), + ) + request_mock = AsyncMock(return_value=response) + fake_skyvern = SimpleNamespace( + _client_wrapper=SimpleNamespace(httpx_client=SimpleNamespace(request=request_mock)), + ) + monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_skyvern) + return request_mock + + +def _google_sheets_definition(block_type: str) -> dict[str, object]: + block: dict[str, object] = { + "block_type": block_type, + "label": f"{block_type}_step", + "spreadsheet_url": "https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit", + "sheet_name": "Sheet1", + "range": "A1:D100", + "credential_id": "{{ google_credential_id }}", + } + if block_type == "google_sheets_read": + block["has_header_row"] = True + elif block_type == "google_sheets_write": + block["write_mode"] = "append" + block["values"] = "{{ output_data | tojson }}" + else: + raise ValueError(f"Unsupported google sheets block type: {block_type}") + + return { + "title": f"{block_type} workflow", + "workflow_definition": { + "parameters": [], + "blocks": [block], + }, + } + + def _heavy_workflow_run_payload(*, include_expanded_outputs: bool = True) -> dict[str, object]: long_url = "https://artifacts.skyvern.example/" + ("x" * 1450) screenshot_urls = [f"{long_url}-{idx}" for idx in range(6)] @@ -86,10 +153,64 @@ def _heavy_workflow_run_payload(*, include_expanded_outputs: bool = True) -> dic } +@pytest.mark.parametrize("block_type", ["google_sheets_read", "google_sheets_write"]) +@pytest.mark.asyncio +async def test_workflow_create_sends_google_sheets_json_definition_as_raw_dict( + monkeypatch: pytest.MonkeyPatch, block_type: str +) -> None: + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) + definition = _google_sheets_definition(block_type) + + result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") + + assert result["ok"] is True, result + sent_definition = request_mock.await_args.kwargs["json"]["json_definition"] + assert type(sent_definition) is dict + assert not hasattr(sent_definition, "model_dump") + sent_block = sent_definition["workflow_definition"]["blocks"][0] + assert sent_block["block_type"] == block_type + assert sent_block["spreadsheet_url"] == "https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit" + + +@pytest.mark.parametrize("block_type", ["google_sheets_read", "google_sheets_write"]) +@pytest.mark.asyncio +async def test_workflow_update_sends_google_sheets_json_definition_as_raw_dict( + monkeypatch: pytest.MonkeyPatch, block_type: str +) -> None: + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) + + async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: + assert workflow_id == "wpid_test" + assert version is None + return { + "proxy_location": "RESIDENTIAL", + "workflow_definition": { + "parameters": [], + "blocks": [], + }, + } + + monkeypatch.setattr(workflow_tools, "_get_workflow_by_id", fake_get_workflow_by_id) + definition = _google_sheets_definition(block_type) + + result = await workflow_tools.skyvern_workflow_update( + workflow_id="wpid_test", + definition=json.dumps(definition), + format="json", + ) + + assert result["ok"] is True, result + sent_definition = request_mock.await_args.kwargs["json"]["json_definition"] + assert type(sent_definition) is dict + assert not hasattr(sent_definition, "model_dump") + sent_block = sent_definition["workflow_definition"]["blocks"][0] + assert sent_block["block_type"] == block_type + assert sent_block["spreadsheet_url"] == "https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit" + + @pytest.mark.asyncio async def test_workflow_create_normalizes_invalid_text_prompt_llm_key(monkeypatch: pytest.MonkeyPatch) -> None: - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Normalize invalid llm_key", @@ -108,19 +229,18 @@ async def test_workflow_create_normalizes_invalid_text_prompt_llm_key(monkeypatc result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") - sent_definition = fake_client.create_workflow.await_args.kwargs["json_definition"] - sent_block = sent_definition.workflow_definition.blocks[0] + sent_definition = request_mock.await_args.kwargs["json"]["json_definition"] + sent_block = sent_definition["workflow_definition"]["blocks"][0] assert result["ok"] is True - assert sent_block.llm_key is None - assert sent_block.model is None - assert "ANTHROPIC_CLAUDE_3_5_SONNET" not in json.dumps(sent_definition.model_dump(mode="json")) + assert sent_block.get("llm_key") is None + assert sent_block.get("model") is None + assert "ANTHROPIC_CLAUDE_3_5_SONNET" not in json.dumps(sent_definition) @pytest.mark.asyncio async def test_workflow_create_preserves_explicit_internal_text_prompt_llm_key(monkeypatch: pytest.MonkeyPatch) -> None: - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Preserve explicit internal llm_key", @@ -143,12 +263,12 @@ async def test_workflow_create_preserves_explicit_internal_text_prompt_llm_key(m ): result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") - sent_definition = fake_client.create_workflow.await_args.kwargs["json_definition"] - sent_block = sent_definition.workflow_definition.blocks[0] + sent_definition = request_mock.await_args.kwargs["json"]["json_definition"] + sent_block = sent_definition["workflow_definition"]["blocks"][0] assert result["ok"] is True - assert sent_block.model is None - assert sent_block.llm_key == "SPECIAL_INTERNAL_KEY" + assert sent_block.get("model") is None + assert sent_block.get("llm_key") == "SPECIAL_INTERNAL_KEY" # --------------------------------------------------------------------------- @@ -174,8 +294,7 @@ async def test_mcp_strips_all_common_hallucinated_llm_keys( monkeypatch: pytest.MonkeyPatch, hallucinated_key: str ) -> None: """MCP workflow creation must strip ANY hallucinated llm_key and default to Skyvern Optimized (null).""" - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Agent-generated workflow", @@ -198,25 +317,23 @@ async def test_mcp_strips_all_common_hallucinated_llm_keys( ): result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="auto") - sent_def = fake_client.create_workflow.await_args.kwargs["json_definition"] - sent_block = sent_def.workflow_definition.blocks[0] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + sent_block = sent_def["workflow_definition"]["blocks"][0] assert result["ok"] is True - assert sent_block.llm_key is None, f"hallucinated key {hallucinated_key!r} was NOT stripped" - assert sent_block.model is None, "should default to Skyvern Optimized (null model)" + assert sent_block.get("llm_key") is None, f"hallucinated key {hallucinated_key!r} was NOT stripped" + assert sent_block.get("model") is None, "should default to Skyvern Optimized (null model)" @pytest.mark.asyncio async def test_workflow_create_preserves_unknown_fields(monkeypatch: pytest.MonkeyPatch) -> None: """Fields not in the internal schema should survive normalization. - The Fern-generated WorkflowCreateYamlRequest uses extra='allow', so unknown - fields are accepted. Our normalization deep-merges the original raw dict with - the normalized output so that future SDK fields not yet mirrored in the - internal schema are preserved at any nesting depth. + Our normalization deep-merges the original raw dict with the backend schema + output so fields not yet mirrored in that schema are preserved at any + nesting depth. """ - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Unknown fields test", @@ -237,19 +354,17 @@ async def test_workflow_create_preserves_unknown_fields(monkeypatch: pytest.Monk result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") assert result["ok"] is True - sent = fake_client.create_workflow.await_args.kwargs["json_definition"] + sent = request_mock.await_args.kwargs["json"]["json_definition"] # Top-level unknown field preserved - assert sent.some_future_sdk_field == "should_survive" + assert sent.get("some_future_sdk_field") == "should_survive" # Nested unknown field inside workflow_definition also preserved via deep merge - wd = sent.workflow_definition - wd_dict = wd.model_dump(mode="json") if hasattr(wd, "model_dump") else wd.__dict__ - assert wd_dict.get("some_nested_future_field") == "also_survives" + wd = sent["workflow_definition"] + assert wd.get("some_nested_future_field") == "also_survives" @pytest.mark.asyncio async def test_workflow_create_defaults_proxy_location_when_omitted(monkeypatch: pytest.MonkeyPatch) -> None: - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Default proxy workflow", @@ -269,16 +384,15 @@ async def test_workflow_create_defaults_proxy_location_when_omitted(monkeypatch: result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") - sent_definition = fake_client.create_workflow.await_args.kwargs["json_definition"] + sent_definition = request_mock.await_args.kwargs["json"]["json_definition"] assert result["ok"] is True - assert sent_definition.proxy_location == "RESIDENTIAL" + assert sent_definition.get("proxy_location") == "RESIDENTIAL" @pytest.mark.asyncio async def test_workflow_create_preserves_block_level_unknown_fields(monkeypatch: pytest.MonkeyPatch) -> None: """Unknown fields inside individual block dicts survive normalization via deep merge.""" - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Block-level unknown fields test", @@ -298,16 +412,14 @@ async def test_workflow_create_preserves_block_level_unknown_fields(monkeypatch: result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") assert result["ok"] is True - sent = fake_client.create_workflow.await_args.kwargs["json_definition"] - sent_block = sent.workflow_definition.blocks[0] - block_dict = sent_block.model_dump(mode="json") if hasattr(sent_block, "model_dump") else sent_block.__dict__ - assert block_dict.get("some_future_block_field") == 42 + sent = request_mock.await_args.kwargs["json"]["json_definition"] + sent_block = sent["workflow_definition"]["blocks"][0] + assert sent_block.get("some_future_block_field") == 42 @pytest.mark.asyncio async def test_workflow_update_preserves_existing_proxy_when_omitted(monkeypatch: pytest.MonkeyPatch) -> None: - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: assert workflow_id == "wpid_test" @@ -338,15 +450,14 @@ async def test_workflow_update_preserves_existing_proxy_when_omitted(monkeypatch format="json", ) - sent_definition = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_definition = request_mock.await_args.kwargs["json"]["json_definition"] assert result["ok"] is True - assert sent_definition.proxy_location == "RESIDENTIAL_AU" + assert sent_definition.get("proxy_location") == "RESIDENTIAL_AU" @pytest.mark.asyncio async def test_workflow_update_defaults_proxy_when_existing_is_null(monkeypatch: pytest.MonkeyPatch) -> None: - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: assert workflow_id == "wpid_test" @@ -377,16 +488,15 @@ async def test_workflow_update_defaults_proxy_when_existing_is_null(monkeypatch: format="json", ) - sent_definition = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_definition = request_mock.await_args.kwargs["json"]["json_definition"] assert result["ok"] is True - assert sent_definition.proxy_location == "RESIDENTIAL" + assert sent_definition.get("proxy_location") == "RESIDENTIAL" @pytest.mark.asyncio async def test_workflow_create_falls_back_on_schema_validation_error(monkeypatch: pytest.MonkeyPatch) -> None: """If the internal schema rejects the payload, normalization is skipped and the raw dict is forwarded.""" - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Schema rejection test", @@ -410,11 +520,10 @@ async def test_workflow_create_falls_back_on_schema_validation_error(monkeypatch result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") assert result["ok"] is True - sent = fake_client.create_workflow.await_args.kwargs["json_definition"] + sent = request_mock.await_args.kwargs["json"]["json_definition"] # Normalization was skipped, so the hallucinated key passes through to the SDK - sent_block = sent.workflow_definition.blocks[0] - block_dict = sent_block.model_dump(mode="json") if hasattr(sent_block, "model_dump") else sent_block.__dict__ - assert block_dict.get("llm_key") == "HALLUCINATED_KEY" + sent_block = sent["workflow_definition"]["blocks"][0] + assert sent_block.get("llm_key") == "HALLUCINATED_KEY" @pytest.mark.parametrize("format_name", ["json", "auto"]) @@ -476,8 +585,7 @@ def test_deep_merge_preserves_block_unknown_fields_when_list_lengths_differ() -> @pytest.mark.asyncio async def test_mcp_text_prompt_without_llm_key_stays_null(monkeypatch: pytest.MonkeyPatch) -> None: """When MCP correctly omits llm_key (Skyvern Optimized), it stays null through the whole pipeline.""" - fake_client = SimpleNamespace(create_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) definition = { "title": "Well-behaved MCP workflow", @@ -494,11 +602,11 @@ async def test_mcp_text_prompt_without_llm_key_stays_null(monkeypatch: pytest.Mo } result = await workflow_tools.skyvern_workflow_create(definition=json.dumps(definition), format="json") - sent_block = fake_client.create_workflow.await_args.kwargs["json_definition"].workflow_definition.blocks[0] + sent_block = request_mock.await_args.kwargs["json"]["json_definition"]["workflow_definition"]["blocks"][0] assert result["ok"] is True - assert sent_block.llm_key is None - assert sent_block.model is None + assert sent_block.get("llm_key") is None + assert sent_block.get("model") is None @pytest.mark.asyncio @@ -644,8 +752,7 @@ async def test_workflow_update_preserves_credential_parameters_when_omitted( """When the update definition omits credential parameters, they should be injected from the existing workflow so the login block keeps working.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -711,13 +818,13 @@ async def test_workflow_update_preserves_credential_parameters_when_omitted( assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] # Verify credential parameter was injected - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 - assert cred_params[0].credential_id == "cred_abc123" - assert cred_params[0].key == "credentials" + assert cred_params[0]["credential_id"] == "cred_abc123" + assert cred_params[0]["key"] == "credentials" @pytest.mark.asyncio @@ -727,8 +834,7 @@ async def test_workflow_update_injects_credential_key_into_block_parameter_keys( """When the update definition omits the credential parameter key from a login block's parameter_keys, the key should be injected from the existing workflow.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -792,17 +898,17 @@ async def test_workflow_update_injects_credential_key_into_block_parameter_keys( assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] # Verify credential parameter was injected - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 - assert cred_params[0].credential_id == "cred_abc123" + assert cred_params[0]["credential_id"] == "cred_abc123" # Verify the login block now references the credential parameter key - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "label", None) == "login_block") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("label") == "login_block") + pkeys = login_block.get("parameter_keys", []) assert "credentials" in pkeys @@ -813,8 +919,7 @@ async def test_workflow_update_injects_credential_key_when_parameter_keys_omitte """When the update definition omits parameter_keys entirely from the block, credential keys should still be injected.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -863,16 +968,16 @@ async def test_workflow_update_injects_credential_key_when_parameter_keys_omitte assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] # Verify credential parameter was injected - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 # Verify the login block now has the credential key - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "label", None) == "login_block") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("label") == "login_block") + pkeys = login_block.get("parameter_keys", []) assert "credentials" in pkeys @@ -883,8 +988,7 @@ async def test_workflow_update_preserves_block_credential_when_param_already_inc """When the update already includes the credential parameter but the block omits the key from parameter_keys, the key should still be injected into the block.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -940,15 +1044,15 @@ async def test_workflow_update_preserves_block_credential_when_param_already_inc assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "label", None) == "login_block") - pkeys = getattr(login_block, "parameter_keys", []) + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("label") == "login_block") + pkeys = login_block.get("parameter_keys", []) assert "credentials" in pkeys # Should not duplicate the credential parameter - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 @@ -959,8 +1063,7 @@ async def test_workflow_update_does_not_duplicate_existing_credential_parameter( """When the update definition already includes the credential parameter, it should NOT be duplicated.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1016,9 +1119,9 @@ async def test_workflow_update_does_not_duplicate_existing_credential_parameter( assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] # Should still be exactly 1, not duplicated assert len(cred_params) == 1 @@ -1030,8 +1133,7 @@ async def test_workflow_update_credential_keys_injected_when_login_block_label_r """When Claude renames a login block label (e.g. 'login_block' -> 'login'), credential parameter_keys should still be injected via type-based matching.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1082,18 +1184,18 @@ async def test_workflow_update_credential_keys_injected_when_login_block_label_r assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] # Credential parameter must be injected - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 - assert cred_params[0].credential_id == "cred_abc123" + assert cred_params[0]["credential_id"] == "cred_abc123" # Login block must have credential key despite label mismatch - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "block_type", None) == "login") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("block_type") == "login") + pkeys = login_block.get("parameter_keys", []) assert "credentials" in pkeys @@ -1104,8 +1206,7 @@ async def test_workflow_update_always_replaces_wrong_credential_id( """When Claude includes a credential parameter with the wrong credential_id, the existing workflow's credential_id should always win.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1161,12 +1262,12 @@ async def test_workflow_update_always_replaces_wrong_credential_id( assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 # The existing (correct) credential_id must win - assert cred_params[0].credential_id == "cred_CORRECT" + assert cred_params[0]["credential_id"] == "cred_CORRECT" @pytest.mark.asyncio @@ -1176,8 +1277,7 @@ async def test_workflow_update_correct_credential_still_works( """When Claude includes the credential parameter correctly, the always-replace strategy should still work (idempotent, no regression).""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1245,21 +1345,21 @@ async def test_workflow_update_correct_credential_still_works( assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 - assert cred_params[0].credential_id == "cred_abc123" + assert cred_params[0]["credential_id"] == "cred_abc123" # Non-credential params should be preserved from the update - wf_params = [p for p in params if getattr(p, "parameter_type", None) == "workflow"] + wf_params = [p for p in params if p.get("parameter_type") == "workflow"] assert len(wf_params) == 1 - assert wf_params[0].default_value == "https://new-url.com" + assert wf_params[0]["default_value"] == "https://new-url.com" # Login block should have the credential key - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "block_type", None) == "login") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("block_type") == "login") + pkeys = login_block.get("parameter_keys", []) assert "credentials" in pkeys @@ -1270,8 +1370,7 @@ async def test_workflow_update_multiple_login_blocks_all_get_credential_keys( """When the update has multiple login blocks, ALL of them should get credential parameter_keys injected (even if labels don't match existing blocks).""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1330,19 +1429,19 @@ async def test_workflow_update_multiple_login_blocks_all_get_credential_keys( assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] - blocks = sent_def.workflow_definition.blocks + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + blocks = sent_def["workflow_definition"]["blocks"] # Both login blocks should have credential keys - login_blocks = [b for b in blocks if getattr(b, "block_type", None) == "login"] + login_blocks = [b for b in blocks if b.get("block_type") == "login"] assert len(login_blocks) == 2 for lb in login_blocks: - pkeys = getattr(lb, "parameter_keys", []) - assert "credentials" in pkeys, f"Login block {getattr(lb, 'label', '?')} missing credential key" + pkeys = lb.get("parameter_keys", []) + assert "credentials" in pkeys, f"Login block {lb.get('label', '?')} missing credential key" # Task block should NOT have credential keys (no label match, not a login block) - task_block = next(b for b in blocks if getattr(b, "block_type", None) == "task") - task_pkeys = getattr(task_block, "parameter_keys", None) or [] + task_block = next(b for b in blocks if b.get("block_type") == "task") + task_pkeys = task_block.get("parameter_keys") or [] assert "credentials" not in task_pkeys @@ -1353,8 +1452,7 @@ async def test_workflow_update_credential_keys_injected_into_login_block_nested_ """When a login block is nested inside a for_loop block, credential parameter_keys should still be injected via type-based matching.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1416,20 +1514,20 @@ async def test_workflow_update_credential_keys_injected_into_login_block_nested_ assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] # Credential parameter must be injected - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 - assert cred_params[0].credential_id == "cred_abc123" + assert cred_params[0]["credential_id"] == "cred_abc123" # The nested login block inside for_loop must have credential keys - blocks = sent_def.workflow_definition.blocks - loop_block = next(b for b in blocks if getattr(b, "block_type", None) == "for_loop") - nested_blocks = getattr(loop_block, "loop_blocks", []) - login_block = next(b for b in nested_blocks if getattr(b, "block_type", None) == "login") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + loop_block = next(b for b in blocks if b.get("block_type") == "for_loop") + nested_blocks = loop_block.get("loop_blocks", []) + login_block = next(b for b in nested_blocks if b.get("block_type") == "login") + pkeys = login_block.get("parameter_keys", []) assert "credentials" in pkeys @@ -1440,8 +1538,7 @@ async def test_workflow_update_login_block_only_gets_credential_type_keys_not_aw """When a workflow has both a credential param and an aws_secret param, login blocks should only get the credential key, not the aws_secret key.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1506,26 +1603,26 @@ async def test_workflow_update_login_block_only_gets_credential_type_keys_not_aw assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] # Both params should be preserved - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] - aws_params = [p for p in params if getattr(p, "parameter_type", None) == "aws_secret"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] + aws_params = [p for p in params if p.get("parameter_type") == "aws_secret"] assert len(cred_params) == 1 assert len(aws_params) == 1 - blocks = sent_def.workflow_definition.blocks + blocks = sent_def["workflow_definition"]["blocks"] # Login block should ONLY get credential key, NOT aws_secret - login_block = next(b for b in blocks if getattr(b, "block_type", None) == "login") - login_pkeys = getattr(login_block, "parameter_keys", []) + login_block = next(b for b in blocks if b.get("block_type") == "login") + login_pkeys = login_block.get("parameter_keys", []) assert "credentials" in login_pkeys assert "api_secret" not in login_pkeys # Task block should get aws_secret via label fallback (label unchanged) - task_block = next(b for b in blocks if getattr(b, "block_type", None) == "task") - task_pkeys = getattr(task_block, "parameter_keys", []) + task_block = next(b for b in blocks if b.get("block_type") == "task") + task_pkeys = task_block.get("parameter_keys", []) assert "api_secret" in task_pkeys @@ -1537,8 +1634,7 @@ async def test_workflow_update_strips_runtime_fields_from_credential_params( (credential_parameter_id, workflow_id, created_at, modified_at, deleted_at), those fields must be stripped before re-injecting into the update definition.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1593,26 +1689,24 @@ async def test_workflow_update_strips_runtime_fields_from_credential_params( assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] - params = sent_def.workflow_definition.parameters - cred_params = [p for p in params if getattr(p, "parameter_type", None) == "credential"] + params = sent_def["workflow_definition"]["parameters"] + cred_params = [p for p in params if p.get("parameter_type") == "credential"] assert len(cred_params) == 1 cred = cred_params[0] # Business fields should be preserved - assert getattr(cred, "key", None) == "credentials" - assert getattr(cred, "credential_id", None) == "cred_abc123" - assert getattr(cred, "description", None) == "Login credentials" + assert cred.get("key") == "credentials" + assert cred.get("credential_id") == "cred_abc123" + assert cred.get("description") == "Login credentials" - # Runtime fields must NOT be present — Fern SDK types use extra="allow" - # so extra fields survive as attributes if passed through - cred_dict = cred.dict() if hasattr(cred, "dict") else cred.__dict__ - assert "credential_parameter_id" not in cred_dict - assert "workflow_id" not in cred_dict - assert "created_at" not in cred_dict - assert "modified_at" not in cred_dict - assert "deleted_at" not in cred_dict + # Runtime fields must NOT be present + assert "credential_parameter_id" not in cred + assert "workflow_id" not in cred + assert "created_at" not in cred + assert "modified_at" not in cred + assert "deleted_at" not in cred @pytest.mark.asyncio @@ -1621,8 +1715,7 @@ async def test_workflow_update_preserves_workflow_credential_id_params_and_injec ) -> None: """Workflow parameters with `credential_id` type should be preserved like direct credential params.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1690,34 +1783,32 @@ async def test_workflow_update_preserves_workflow_credential_id_params_and_injec assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] - params = sent_def.workflow_definition.parameters + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + params = sent_def["workflow_definition"]["parameters"] workflow_cred_params = [ p for p in params - if getattr(p, "parameter_type", None) == "workflow" - and str(getattr(p, "workflow_parameter_type", None)) == "credential_id" + if p.get("parameter_type") == "workflow" and str(p.get("workflow_parameter_type")) == "credential_id" ] assert len(workflow_cred_params) == 1 workflow_cred = workflow_cred_params[0] - assert getattr(workflow_cred, "key", None) == "my_creds" - assert getattr(workflow_cred, "default_value", None) == "cred_abc123" + assert workflow_cred.get("key") == "my_creds" + assert workflow_cred.get("default_value") == "cred_abc123" - workflow_cred_dict = workflow_cred.dict() if hasattr(workflow_cred, "dict") else workflow_cred.__dict__ - assert "workflow_parameter_id" not in workflow_cred_dict - assert "workflow_id" not in workflow_cred_dict - assert "created_at" not in workflow_cred_dict - assert "modified_at" not in workflow_cred_dict - assert "deleted_at" not in workflow_cred_dict + assert "workflow_parameter_id" not in workflow_cred + assert "workflow_id" not in workflow_cred + assert "created_at" not in workflow_cred + assert "modified_at" not in workflow_cred + assert "deleted_at" not in workflow_cred - workflow_params = [p for p in params if getattr(p, "parameter_type", None) == "workflow"] - url_param = next(p for p in workflow_params if getattr(p, "key", None) == "url_input") - assert getattr(url_param, "default_value", None) == "https://new-url.com" + workflow_params = [p for p in params if p.get("parameter_type") == "workflow"] + url_param = next(p for p in workflow_params if p.get("key") == "url_input") + assert url_param.get("default_value") == "https://new-url.com" - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "block_type", None) == "login") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("block_type") == "login") + pkeys = login_block.get("parameter_keys", []) assert "my_creds" in pkeys @@ -1727,8 +1818,7 @@ async def test_workflow_update_always_replaces_wrong_workflow_credential_id_defa ) -> None: """Credential-id workflow params should keep the existing default credential on MCP updates.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1785,16 +1875,15 @@ async def test_workflow_update_always_replaces_wrong_workflow_credential_id_defa assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] - params = sent_def.workflow_definition.parameters + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] + params = sent_def["workflow_definition"]["parameters"] workflow_cred_params = [ p for p in params - if getattr(p, "parameter_type", None) == "workflow" - and str(getattr(p, "workflow_parameter_type", None)) == "credential_id" + if p.get("parameter_type") == "workflow" and str(p.get("workflow_parameter_type")) == "credential_id" ] assert len(workflow_cred_params) == 1 - assert getattr(workflow_cred_params[0], "default_value", None) == "cred_CORRECT" + assert workflow_cred_params[0].get("default_value") == "cred_CORRECT" @pytest.mark.asyncio @@ -1803,8 +1892,7 @@ async def test_workflow_update_injects_onepassword_key_into_login_block_paramete ) -> None: """Login blocks should keep external credential keys like onepassword after MCP edits.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1855,18 +1943,18 @@ async def test_workflow_update_injects_onepassword_key_into_login_block_paramete assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] - params = sent_def.workflow_definition.parameters - onepassword_params = [p for p in params if getattr(p, "parameter_type", None) == "onepassword"] + params = sent_def["workflow_definition"]["parameters"] + onepassword_params = [p for p in params if p.get("parameter_type") == "onepassword"] assert len(onepassword_params) == 1 - assert getattr(onepassword_params[0], "key", None) == "site_login_cred" - assert getattr(onepassword_params[0], "vault_id", None) == "vault_123" - assert getattr(onepassword_params[0], "item_id", None) == "item_456" + assert onepassword_params[0].get("key") == "site_login_cred" + assert onepassword_params[0].get("vault_id") == "vault_123" + assert onepassword_params[0].get("item_id") == "item_456" - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "block_type", None) == "login") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("block_type") == "login") + pkeys = login_block.get("parameter_keys", []) assert "site_login_cred" in pkeys @@ -1876,8 +1964,7 @@ async def test_workflow_update_injects_bitwarden_login_key_into_login_block_para ) -> None: """Login blocks should keep bitwarden_login_credential keys after MCP edits.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -1936,16 +2023,16 @@ async def test_workflow_update_injects_bitwarden_login_key_into_login_block_para assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] - params = sent_def.workflow_definition.parameters - bw_params = [p for p in params if getattr(p, "parameter_type", None) == "bitwarden_login_credential"] + params = sent_def["workflow_definition"]["parameters"] + bw_params = [p for p in params if p.get("parameter_type") == "bitwarden_login_credential"] assert len(bw_params) == 1 - assert getattr(bw_params[0], "key", None) == "portal_login" + assert bw_params[0].get("key") == "portal_login" - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "block_type", None) == "login") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("block_type") == "login") + pkeys = login_block.get("parameter_keys", []) assert "portal_login" in pkeys @@ -1955,8 +2042,7 @@ async def test_workflow_update_injects_azure_vault_key_into_login_block_paramete ) -> None: """Login blocks should keep azure_vault_credential keys after MCP edits.""" - fake_client = SimpleNamespace(update_workflow=AsyncMock(return_value=_fake_workflow_response())) - monkeypatch.setattr(workflow_tools, "get_skyvern", lambda: fake_client) + request_mock = _patch_skyvern_http(monkeypatch, response_payload=_fake_workflow_dict()) async def fake_get_workflow_by_id(workflow_id: str, version: int | None = None) -> dict[str, object]: return { @@ -2014,14 +2100,14 @@ async def test_workflow_update_injects_azure_vault_key_into_login_block_paramete assert result["ok"] is True - sent_def = fake_client.update_workflow.await_args.kwargs["json_definition"] + sent_def = request_mock.await_args.kwargs["json"]["json_definition"] - params = sent_def.workflow_definition.parameters - az_params = [p for p in params if getattr(p, "parameter_type", None) == "azure_vault_credential"] + params = sent_def["workflow_definition"]["parameters"] + az_params = [p for p in params if p.get("parameter_type") == "azure_vault_credential"] assert len(az_params) == 1 - assert getattr(az_params[0], "key", None) == "azure_creds" + assert az_params[0].get("key") == "azure_creds" - blocks = sent_def.workflow_definition.blocks - login_block = next(b for b in blocks if getattr(b, "block_type", None) == "login") - pkeys = getattr(login_block, "parameter_keys", []) + blocks = sent_def["workflow_definition"]["blocks"] + login_block = next(b for b in blocks if b.get("block_type") == "login") + pkeys = login_block.get("parameter_keys", []) assert "azure_creds" in pkeys diff --git a/tests/unit/test_validation_span_attrs.py b/tests/unit/test_validation_span_attrs.py new file mode 100644 index 000000000..314ad30ef --- /dev/null +++ b/tests/unit/test_validation_span_attrs.py @@ -0,0 +1,182 @@ +"""Tests for the ``validation.decision`` / ``validation.reasoning_kind`` span +attributes (SKY-9174, Part D.3). + +The two attributes give us a post-merge logfire signal for when a validation +block's LLM reasons literally and/or terminates — the failure mode Part D aims +to reduce. Query shape:: + + SELECT COUNT(*) FROM records + WHERE span_name = 'skyvern.agent.step_body' + AND attributes->>'validation.decision' = 'terminate' + AND attributes->>'validation.reasoning_kind' = 'literal' + AND start_timestamp > now() - INTERVAL '24 hours'; + +Pre-fix this count should be non-trivial; post-fix it should trend to zero on +the copilot-v2 cohort. These tests cover the attribute-writing logic directly +(the helper is pure, so we don't need to drive the full agent step). +""" + +from __future__ import annotations + +from datetime import UTC, datetime + +import opentelemetry.trace as otel_trace +import pytest +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +from skyvern.forge.agent import record_validation_span_attrs +from skyvern.forge.sdk.db.enums import TaskType +from skyvern.webeye.actions.actions import ( + Action, + ActionType, + ClickAction, + CompleteAction, + TerminateAction, +) +from tests.unit.helpers import make_organization, make_task + +STEP_SPAN_NAME = "skyvern.agent.validation_step_body_fixture" + + +def _validation_task() -> object: + now = datetime.now(UTC) + org = make_organization(now) + return make_task(now, org, task_type=TaskType.validation) + + +def _general_task() -> object: + now = datetime.now(UTC) + org = make_organization(now) + return make_task(now, org, task_type=TaskType.general) + + +def _run_with_span(task: object, actions: list[Action]) -> dict: + """Start a span, invoke the helper inside it, end the span. Return the + span's attribute dict via the in-memory exporter.""" + tracer = otel_trace.get_tracer("sky-9174-test") + with tracer.start_as_current_span(STEP_SPAN_NAME) as span: + record_validation_span_attrs(span, task, actions) + return {} # attrs read from the exporter by the caller + + +def _span_attrs(span_exporter: InMemorySpanExporter) -> dict: + span = next((s for s in span_exporter.get_finished_spans() if s.name == STEP_SPAN_NAME), None) + assert span is not None, "expected fixture span to be recorded" + return dict(span.attributes or {}) + + +def _complete_action(reasoning: str) -> CompleteAction: + return CompleteAction( + reasoning=reasoning, + intention=reasoning, + action_type=ActionType.COMPLETE, + ) + + +def _terminate_action(reasoning: str) -> TerminateAction: + return TerminateAction( + reasoning=reasoning, + intention=reasoning, + action_type=ActionType.TERMINATE, + ) + + +def test_complete_with_semantic_reasoning_records_semantic(span_exporter: InMemorySpanExporter) -> None: + task = _validation_task() + actions = [_complete_action("The current page shows a thank-you confirmation.")] + _run_with_span(task, actions) + attrs = _span_attrs(span_exporter) + assert attrs.get("validation.decision") == "complete" + assert attrs.get("validation.reasoning_kind") == "semantic" + + +def test_terminate_with_literal_reasoning_records_literal(span_exporter: InMemorySpanExporter) -> None: + """The regression we care about most: LLM terminated because an exact + string wasn't found. This is the combination (terminate, literal) that + Part D aims to drive toward zero.""" + task = _validation_task() + actions = [ + _terminate_action( + "The page does not contain the exact complete-criterion text 'Your message has been sent'. TERMINATE." + ) + ] + _run_with_span(task, actions) + attrs = _span_attrs(span_exporter) + assert attrs.get("validation.decision") == "terminate" + assert attrs.get("validation.reasoning_kind") == "literal" + + +def test_terminate_with_semantic_reasoning_records_semantic(span_exporter: InMemorySpanExporter) -> None: + task = _validation_task() + actions = [_terminate_action("An error banner surfaced at the top of the page saying the submission failed.")] + _run_with_span(task, actions) + attrs = _span_attrs(span_exporter) + assert attrs.get("validation.decision") == "terminate" + assert attrs.get("validation.reasoning_kind") == "semantic" + + +def test_complete_with_literal_reasoning_records_literal(span_exporter: InMemorySpanExporter) -> None: + """Symmetric — a literal COMPLETE is harmless but we still tag it, because + the post-merge dashboard cares about the distribution across both axes, + not just the terminate one.""" + task = _validation_task() + actions = [_complete_action("The page contains the exact phrase 'Your message has been sent'.")] + _run_with_span(task, actions) + attrs = _span_attrs(span_exporter) + assert attrs.get("validation.decision") == "complete" + assert attrs.get("validation.reasoning_kind") == "literal" + + +def test_non_validation_task_does_not_tag_span(span_exporter: InMemorySpanExporter) -> None: + """Guard against accidental tagging of non-validation step spans — those + span attributes are reserved for TaskType.validation.""" + task = _general_task() + actions = [_complete_action("The current page shows a thank-you confirmation.")] + _run_with_span(task, actions) + attrs = _span_attrs(span_exporter) + assert "validation.decision" not in attrs + assert "validation.reasoning_kind" not in attrs + + +def test_non_decisive_action_does_not_tag_span(span_exporter: InMemorySpanExporter) -> None: + """Validation tasks whose first action isn't a Complete/Terminate (unusual + but possible during partial parsing) should not produce tagged attrs.""" + task = _validation_task() + # A ClickAction stands in for any non-DecisiveAction leading-first. + non_decisive = ClickAction(action_type=ActionType.CLICK, element_id="AAAB", reasoning="click") + _run_with_span(task, [non_decisive]) + attrs = _span_attrs(span_exporter) + assert "validation.decision" not in attrs + assert "validation.reasoning_kind" not in attrs + + +def test_empty_actions_list_does_not_tag_span(span_exporter: InMemorySpanExporter) -> None: + task = _validation_task() + _run_with_span(task, []) + attrs = _span_attrs(span_exporter) + assert "validation.decision" not in attrs + assert "validation.reasoning_kind" not in attrs + + +def test_missing_reasoning_defaults_to_semantic(span_exporter: InMemorySpanExporter) -> None: + """Empty/None reasoning shouldn't crash — absence of literal signals means + semantic by the helper's rule.""" + task = _validation_task() + actions = [_complete_action("")] + _run_with_span(task, actions) + attrs = _span_attrs(span_exporter) + assert attrs.get("validation.decision") == "complete" + assert attrs.get("validation.reasoning_kind") == "semantic" + + +@pytest.mark.parametrize( + "signal", + ["exact", "literal", "verbatim", "word-for-word", "word for word"], +) +def test_every_literal_signal_flags_reasoning(signal: str, span_exporter: InMemorySpanExporter) -> None: + """Each configured signal, on its own, must classify reasoning as literal.""" + task = _validation_task() + actions = [_terminate_action(f"The criterion does not appear {signal} on the page.")] + _run_with_span(task, actions) + attrs = _span_attrs(span_exporter) + assert attrs.get("validation.reasoning_kind") == "literal", signal diff --git a/tests/unit/test_verification_span_attrs.py b/tests/unit/test_verification_span_attrs.py new file mode 100644 index 000000000..9adb830f9 --- /dev/null +++ b/tests/unit/test_verification_span_attrs.py @@ -0,0 +1,79 @@ +"""Tests for the ``verification.reasoning_kind`` span attribute (SKY-9174, Part E.2). + +Part A already wrote ``verification.status`` and ``verification.template`` on +``skyvern.agent.complete_verify`` spans. Part E adds one more: +``verification.reasoning_kind`` (``literal`` | ``semantic``) derived from the +verifier LLM's ``thoughts`` text via the shared ``_classify_reasoning_kind`` +heuristic. Post-fix logfire query:: + + SELECT COUNT(*) FROM records + WHERE span_name = 'skyvern.agent.complete_verify' + AND attributes->>'verification.status' != 'complete' + AND attributes->>'verification.reasoning_kind' = 'literal' + AND start_timestamp > now() - INTERVAL '24 hours'; + +Pre-fix, a single reproducing run produces double-digit rows with +``reasoning_kind = 'literal'``. Post-fix, that count should trend to zero. +""" + +from __future__ import annotations + +import opentelemetry.trace as otel_trace +import pytest +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +from skyvern.forge.agent import record_verification_span_attrs + +SPAN_NAME = "skyvern.agent.verification_fixture" + + +def _run_with_span(thoughts: str | None) -> None: + tracer = otel_trace.get_tracer("sky-9174-part-e-test") + with tracer.start_as_current_span(SPAN_NAME) as span: + record_verification_span_attrs(span, thoughts) + + +def _span_attrs(span_exporter: InMemorySpanExporter) -> dict: + span = next((s for s in span_exporter.get_finished_spans() if s.name == SPAN_NAME), None) + assert span is not None, "expected fixture span to be recorded" + return dict(span.attributes or {}) + + +def test_literal_reasoning_records_literal(span_exporter: InMemorySpanExporter) -> None: + """The regression we care about most: verifier insisted on finding the + criterion's exact wording on the page and returned ``continue``. This is + the (verifier, literal) combination Part E aims to drive toward zero.""" + _run_with_span("The page does not contain the exact phrase 'Your message has been sent'. user_goal_achieved=false.") + attrs = _span_attrs(span_exporter) + assert attrs.get("verification.reasoning_kind") == "literal" + + +def test_semantic_reasoning_records_semantic(span_exporter: InMemorySpanExporter) -> None: + _run_with_span("The page renders a thank-you confirmation, satisfying the goal's intent.") + attrs = _span_attrs(span_exporter) + assert attrs.get("verification.reasoning_kind") == "semantic" + + +def test_empty_reasoning_defaults_to_semantic(span_exporter: InMemorySpanExporter) -> None: + _run_with_span("") + attrs = _span_attrs(span_exporter) + assert attrs.get("verification.reasoning_kind") == "semantic" + + +def test_none_reasoning_defaults_to_semantic(span_exporter: InMemorySpanExporter) -> None: + _run_with_span(None) + attrs = _span_attrs(span_exporter) + assert attrs.get("verification.reasoning_kind") == "semantic" + + +@pytest.mark.parametrize( + "signal", + ["exact", "literal", "verbatim", "word-for-word", "word for word"], +) +def test_every_literal_signal_flags_reasoning(signal: str, span_exporter: InMemorySpanExporter) -> None: + """Shared classifier: each signal used by ``record_validation_span_attrs`` + also flags verifier reasoning. Guards against future drift between the two + callers.""" + _run_with_span(f"The goal does not appear {signal} on the page.") + attrs = _span_attrs(span_exporter) + assert attrs.get("verification.reasoning_kind") == "literal", signal diff --git a/uv.lock b/uv.lock index 5b7c2fa66..0e36b207f 100644 --- a/uv.lock +++ b/uv.lock @@ -13,10 +13,6 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -[options] -exclude-newer = "2026-04-15T04:02:43.170441Z" -exclude-newer-span = "P7D" - [manifest] constraints = [ { name = "authlib", specifier = ">=1.6.9" }, @@ -437,14 +433,15 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.9" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, + { name = "joserfc" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/82/4d0603f30c1b4629b1f091bb266b0d7986434891d6940a8c87f8098db24e/authlib-1.7.0.tar.gz", hash = "sha256:b3e326c9aa9cc3ea95fe7d89fd880722d3608da4d00e8a27e061e64b48d801d5", size = 175890, upload-time = "2026-04-18T11:00:28.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/ca/48/c954218b2a250e23f178f10167c4173fecb5a75d2c206f0a67ba58006c26/authlib-1.7.0-py2.py3-none-any.whl", hash = "sha256:e36817afb02f6f0b6bf55f150782499ddd6ddf44b402bb055d3263cc65ac9ae0", size = 258779, upload-time = "2026-04-18T11:00:26.64Z" }, ] [[package]] @@ -518,16 +515,16 @@ wheels = [ [[package]] name = "azure-keyvault-secrets" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/e5/3074e581b6e8923c4a1f2e42192ea6f390bb52de3600c68baaaed529ef05/azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36", size = 129695, upload-time = "2025-06-16T22:52:20.986Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/b8/03c7b4edd1e3355ad5fffb70e68af70cd09542963f45cf3a2aa9fb3930b5/azure_keyvault_secrets-4.11.0.tar.gz", hash = "sha256:ac14727b9159cca353173ec5a454d8d7b192a6f2f5e7eb540f9fbcf914fa0ca0", size = 112291, upload-time = "2026-04-17T00:52:12.422Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/94/7c902e966b28e7cb5080a8e0dd6bffc22ba44bc907f09c4c633d2b7c4f6a/azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9", size = 125237, upload-time = "2025-06-16T22:52:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/41c513917c0e8dc68de0c33578c6a0668b4a09eb4dc3e2782d382dc2947f/azure_keyvault_secrets-4.11.0-py3-none-any.whl", hash = "sha256:d8543b710423569bd00ada2a2533fe83d52d8e1fe026e1c47f41a3bc0fc73ef5", size = 103148, upload-time = "2026-04-17T00:52:13.796Z" }, ] [[package]] @@ -662,16 +659,16 @@ wheels = [ [[package]] name = "build" -version = "1.4.2" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/1d/ab15c8ac57f4ee8778d7633bc6685f808ab414437b8644f555389cdc875e/build-1.4.2.tar.gz", hash = "sha256:35b14e1ee329c186d3f08466003521ed7685ec15ecffc07e68d706090bf161d1", size = 83433, upload-time = "2026-03-25T14:20:27.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/ec/bf5ae0a7e5ab57abe8aabdd0759c971883895d1a20c49ae99f8146840c3c/build-1.4.4.tar.gz", hash = "sha256:f832ae053061f3fb524af812dc94b8b84bac6880cd587630e3b5d91a6a9c1703", size = 89220, upload-time = "2026-04-22T20:53:44.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl", hash = "sha256:7a4d8651ea877cb2a89458b1b198f2e69f536c95e89129dbf5d448045d60db88", size = 24643, upload-time = "2026-03-25T14:20:26.568Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-py3-none-any.whl", hash = "sha256:8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d", size = 25921, upload-time = "2026-04-22T20:53:43.251Z" }, ] [[package]] @@ -706,11 +703,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] @@ -847,14 +844,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.2" +version = "8.3.3" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -956,7 +953,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.10.2" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -964,9 +961,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/2c/fced34890f6e5a93a4b7afb2c71e8eee2a0719fb26193a0abf159ecb714d/cyclopts-4.10.2.tar.gz", hash = "sha256:d7b950457ef2563596d56331f80cbbbf86a2772535fb8b315c4f03bc7e6127f1", size = 166664, upload-time = "2026-04-08T23:57:45.805Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/fa/eff8f1abae783bade9b5e9bafafd0040d4dbf51988f9384bfdc0326ba1fc/cyclopts-4.11.0.tar.gz", hash = "sha256:1ffcb9990dbd56b90da19980d31596de9e99019980a215a5d76cf88fe452e94d", size = 170690, upload-time = "2026-04-23T00:23:36.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/bd/05055d8360cef0757d79367157f3b15c0a0715e81e08f86a04018ec045f0/cyclopts-4.10.2-py3-none-any.whl", hash = "sha256:a1f2d6f8f7afac9456b48f75a40b36658778ddc9c6d406b520d017ae32c990fe", size = 204314, upload-time = "2026-04-08T23:57:46.969Z" }, + { url = "https://files.pythonhosted.org/packages/7c/37/197db187c260d24d4be1f09d427f59f3fb9a89bcf1354e23865c7bff7607/cyclopts-4.11.0-py3-none-any.whl", hash = "sha256:34318e3823b44b5baa754a5e37ec70a5c17dc81c65e4295ed70e17bc1aeae50d", size = 208494, upload-time = "2026-04-23T00:23:34.948Z" }, ] [[package]] @@ -1051,11 +1048,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] @@ -1112,7 +1109,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.135.3" +version = "0.135.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -1121,9 +1118,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/1e/957e66314411255bd5c4d2c9f5259c3b3b44d8b50a702d38577c950f9d92/fastapi-0.135.4.tar.gz", hash = "sha256:d87c41b0a7bcaa6f14629d73fe48e360821605c7b6d518caacbc00dcf8fa5e0e", size = 396670, upload-time = "2026-04-16T11:39:29.385Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, + { url = "https://files.pythonhosted.org/packages/ef/81/32a3db8ed89cd1e616deb31aa0d9c6ec58bb712a20d4c750757d320f8ca8/fastapi-0.135.4-py3-none-any.whl", hash = "sha256:539d3531f8aba9b286ab44658344553f4a4adc218529137501e5d97be071a78b", size = 117403, upload-time = "2026-04-16T11:39:30.896Z" }, ] [[package]] @@ -1137,12 +1134,13 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.2.3" +version = "3.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, + { name = "griffelib" }, { name = "httpx" }, { name = "jsonref" }, { name = "jsonschema-path" }, @@ -1162,9 +1160,9 @@ dependencies = [ { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/42/7eed0a38e3b7a386805fecacf8a5a9353a2b3040395ef9e30e585d8549ac/fastmcp-3.2.3.tar.gz", hash = "sha256:4f02ae8b00227285a0cf6544dea1db29b022c8cdd8d3dfdec7118540210ae60a", size = 26328743, upload-time = "2026-04-09T22:05:03.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/13/29544fbc6dfe45ea38046af0067311e0bad7acc7d1f2ad38bb08f2409fe2/fastmcp-3.2.4.tar.gz", hash = "sha256:083ecb75b44a4169e7fc0f632f94b781bdb0ff877c6b35b9877cbb566fd4d4d1", size = 28746127, upload-time = "2026-04-14T01:42:24.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/48/84b6dcba793178a44b9d99b4def6cd62f870dcfc5bb7b9153ac390135812/fastmcp-3.2.3-py3-none-any.whl", hash = "sha256:cc50af6eed1f62ed8b6ebf4987286d8d1d006f08d5bec739d5c7fb76160e0911", size = 707260, upload-time = "2026-04-09T22:05:01.225Z" }, + { url = "https://files.pythonhosted.org/packages/cf/76/b310d52fa0e30d39bd937eb58ec2c1f1ea1b5f519f0575e9dd9612f01deb/fastmcp-3.2.4-py3-none-any.whl", hash = "sha256:e6c9c429171041455e47ab94bb3f83c4657622a0ec28922f6940053959bd58a9", size = 728599, upload-time = "2026-04-14T01:42:26.85Z" }, ] [[package]] @@ -1210,11 +1208,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] [[package]] @@ -1447,7 +1445,7 @@ wheels = [ [[package]] name = "google-cloud-aiplatform" -version = "1.147.0" +version = "1.148.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -1463,9 +1461,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/93/9bfcaaf1ceab12999a881ccf69ebd9b30f467ec5623989c66894e81fc139/google_cloud_aiplatform-1.147.0.tar.gz", hash = "sha256:b2e1b669ba37f02426e03eb13187eebf4cbfeaa0a3bfed37b5578abb375ab689", size = 10235245, upload-time = "2026-04-09T17:14:49.179Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/f3/b2a9417014c93858a2e3266134f931eefd972c2d410b25d7b8782fc6f143/google_cloud_aiplatform-1.148.1.tar.gz", hash = "sha256:75d605fba34e68714bd08e1e482755d0a6e3ae972805f809d088e686c30879e7", size = 10278758, upload-time = "2026-04-17T23:45:26.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/d2/1c1c582f6bbed9bbc0daa5acf3a5d98751ca8bc48584548d28569b8ce1a7/google_cloud_aiplatform-1.147.0-py2.py3-none-any.whl", hash = "sha256:29f7ae020718d3c45094f0475464e06a97f81b1572bea150ae6a1b22c5f45997", size = 8408951, upload-time = "2026-04-09T17:14:45.482Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/e3515d7bbba602c2b0f6a0da5431785e897252443682e4735d0e6873dc8f/google_cloud_aiplatform-1.148.1-py2.py3-none-any.whl", hash = "sha256:035101e2d8e65c6a706cc3930b2452de7ddcbde50dd130320fcea0d8b03b0c5a", size = 8434481, upload-time = "2026-04-17T23:45:22.919Z" }, ] [[package]] @@ -1560,7 +1558,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.72.0" +version = "1.73.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1574,9 +1572,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/20/2aff5ea3cd7459f85101d119c136d9ca4369fcda3dcf0cfee89b305611a4/google_genai-1.72.0.tar.gz", hash = "sha256:abe7d3aecfafb464b904e3a09c81b626fb425e160e123e71a5125a7021cea7b2", size = 522844, upload-time = "2026-04-09T21:35:46.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/d8/40f5f107e5a2976bbac52d421f04d14fc221b55a8f05e66be44b2f739fe6/google_genai-1.73.1.tar.gz", hash = "sha256:b637e3a3b9e2eccc46f27136d470165803de84eca52abfed2e7352081a4d5a15", size = 530998, upload-time = "2026-04-14T21:06:19.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/3d/9f70246114cdf56a2615a40428ced08bc844f5a26247fe812b2f0dd4eaca/google_genai-1.72.0-py3-none-any.whl", hash = "sha256:ea861e4c6946e3185c24b40d95503e088fc230a73a71fec0ef78164b369a8489", size = 764230, upload-time = "2026-04-09T21:35:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/508e0528015240d710c6763f7c89ff44fab9a94a80b4377e265d692cbfd6/google_genai-1.73.1-py3-none-any.whl", hash = "sha256:af2d2287d25e42a187de19811ef33beb2e347c7e2bdb4dc8c467d78254e43a2c", size = 783595, upload-time = "2026-04-14T21:06:17.464Z" }, ] [[package]] @@ -1953,7 +1951,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.10.1" +version = "1.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1966,9 +1964,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/28/baf5d745559503ce8d28cf5bc9551f5ac59158eafd7b6a6afff0bcdb0f50/huggingface_hub-1.10.1.tar.gz", hash = "sha256:696c53cf9c2ac9befbfb5dd41d05392a031c69fc6930d1ed9671debd405b6fff", size = 758094, upload-time = "2026-04-09T15:01:18.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/89/e7aa12d8a6b9259bed10671abb25ae6fa437c0f88a86ecbf59617bae7759/huggingface_hub-1.11.0.tar.gz", hash = "sha256:15fb3713c7f9cdff7b808a94fd91664f661ab142796bb48c9cd9493e8d166278", size = 761749, upload-time = "2026-04-16T13:07:39.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/8c/c7a33f3efaa8d6a5bc40e012e5ecc2d72c2e6124550ca9085fe0ceed9993/huggingface_hub-1.10.1-py3-none-any.whl", hash = "sha256:6b981107a62fbe68c74374418983399c632e35786dcd14642a9f2972633c8b5a", size = 642630, upload-time = "2026-04-09T15:01:17.35Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/4f3f8997d1ea7fe0146b343e5e14bd065fa87af790d07e5576d31b31cc18/huggingface_hub-1.11.0-py3-none-any.whl", hash = "sha256:42a6de0afbfeb5e022222d36398f029679db4eb4778801aafda32257ae9131ab", size = 645499, upload-time = "2026-04-16T13:07:37.716Z" }, ] [[package]] @@ -1985,20 +1983,20 @@ wheels = [ [[package]] name = "identify" -version = "2.6.18" +version = "2.6.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] @@ -2193,62 +2191,65 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, + { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, ] [[package]] @@ -2262,14 +2263,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.6.3" +version = "1.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/90/b8cc8635c4ce2e5e8104bf26ef147f6e599478f6329107283cdc53aae97f/joserfc-1.6.3.tar.gz", hash = "sha256:c00c2830db969b836cba197e830e738dd9dda0955f1794e55d3c636f17f5c9a6", size = 229090, upload-time = "2026-02-25T15:33:38.167Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4f/124b3301067b752f44f292f0b9a74e837dd75ff863ee39500a082fc4c733/joserfc-1.6.3-py3-none-any.whl", hash = "sha256:6beab3635358cbc565cb94fb4c53d0557e6d10a15b933e2134939351590bda9a", size = 70465, upload-time = "2026-02-25T15:33:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" }, ] [[package]] @@ -2415,7 +2416,7 @@ wheels = [ [[package]] name = "jupyter-events" -version = "0.12.0" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"] }, @@ -2427,9 +2428,9 @@ dependencies = [ { name = "rfc3986-validator" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, ] [[package]] @@ -2745,80 +2746,80 @@ wheels = [ [[package]] name = "lxml" -version = "6.0.3" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/42/149c7747977db9d68faee960c1a3391eb25e94d4bb677f8e2df8328e4098/lxml-6.0.3.tar.gz", hash = "sha256:a1664c5139755df44cab3834f4400b331b02205d62d3fdcb1554f63439bf3372", size = 4237567, upload-time = "2026-04-09T14:39:09.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ce/8a0b4747bb5dd47fec3443f0506a2a2d4f58946d7176bc3fdcae781ac666/lxml-6.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c8184fdb2259bda1db2db9d6e25f667769afc2531830b4fa29f83f66a7872dea", size = 8524445, upload-time = "2026-04-09T14:34:14.244Z" }, - { url = "https://files.pythonhosted.org/packages/e5/14/b74a06da69d212d1ac27e4bcf124e966d1d63c4d23522add86fbcf20324e/lxml-6.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b0f01fb8bdcaf4aa69cf55b2b2f8ef722e4423e1c020e7250dcb89a1d5db38e", size = 4594891, upload-time = "2026-04-09T14:34:17.123Z" }, - { url = "https://files.pythonhosted.org/packages/39/9a/364392e9740ddcdba380c5dbb79464956aadf81135344d57153631c8e4a2/lxml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fab00cef83d4f9d76c5e0722346e84bc174b071d68b4f725aeb0bf3877b9e6a6", size = 4922596, upload-time = "2026-04-09T14:34:19.632Z" }, - { url = "https://files.pythonhosted.org/packages/24/b6/6e4a53869a8e031dc5ea564a9857f6dd520a05412aea8d1b6565e8b2d43d/lxml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f753db5785ce019d7b25bb75638ef5a42a0e208aa9f19933262134e668ca6af", size = 5067033, upload-time = "2026-04-09T14:34:22.015Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cc/12035c0d104fbe64e56e7b2cd9d4942ffa2a1689f093f44de0eef73538ae/lxml-6.0.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27e317e554bc6086a082688ddf137437e5f7f20ffdd736a6f5b4e3ed1ecf1247", size = 5000434, upload-time = "2026-04-09T14:34:23.934Z" }, - { url = "https://files.pythonhosted.org/packages/73/37/b9f9b28b542d0e62bb9353753cbec7e321f7394fea10470c6b8e5739b61d/lxml-6.0.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:feb5b9ed7d0510663a78b94f2b417a41c41b42a7bb157ef398ef9d78e6f0fd50", size = 5201705, upload-time = "2026-04-09T14:34:26.328Z" }, - { url = "https://files.pythonhosted.org/packages/2d/26/9473de56eb74293c7061ff1a6ac352d5b89c83067f315accd73cf97a3b07/lxml-6.0.3-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:51014ee2ab2091dcd9cdef92532f0a1addb7c2cc52a2bd70682e441363de5c0d", size = 5329269, upload-time = "2026-04-09T14:34:29.563Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a3/502f97b6221e0958da94fde5eb17119f2104694a88126ef82fa189d5d7a4/lxml-6.0.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:abc39c4fb67f029400608f9a3a4a3f351ccb3c061b05fd3ad113e4cfbba8a8ee", size = 4658312, upload-time = "2026-04-09T14:34:31.62Z" }, - { url = "https://files.pythonhosted.org/packages/d1/26/935d0297d1c272282e950986f14f044c8e4c34e60a8774bc993d26ddcf32/lxml-6.0.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:38652c280cf50cc5cf955e3d0b691fa6a97046d84407bbae340d8e353f9014ef", size = 5264811, upload-time = "2026-04-09T14:34:33.566Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a0/4755420775ded42b4cc9017357ce72ee7cd08fbfb72da3ac7e48fa2326bb/lxml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3de55b53f69ffa2fcfd897bd8a7e62f0f88a40a8a0c544e171e813f9d4ddbf5", size = 5043997, upload-time = "2026-04-09T14:34:35.506Z" }, - { url = "https://files.pythonhosted.org/packages/b7/54/61f21fcf0b5c0f30e58c369aacfa01f5a21ef0f8c9c773c413010c18a705/lxml-6.0.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd4f70e091f2df300396bc9ce36963f90b87611324c2ca750072a6e6375beba2", size = 4711595, upload-time = "2026-04-09T14:34:38.195Z" }, - { url = "https://files.pythonhosted.org/packages/ca/9e/b9f73274a7e3819c821033ea9d8e777b297fcbe789765948d8c9d4fb9cfc/lxml-6.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c157bfef4e3b19688eb4da783c5bfabf5a3ac1ac8d317e0906f3feb18d4c89b7", size = 5251294, upload-time = "2026-04-09T14:34:40.461Z" }, - { url = "https://files.pythonhosted.org/packages/38/8c/73e463041bad522c348c8a8c908a63c32f80215cff596210bbf24d69b3ee/lxml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8d10a75e4d0a6a9ac2fec2f7ade682f468b51935102c70dab638fa4e94ffcb04", size = 5224927, upload-time = "2026-04-09T14:34:42.986Z" }, - { url = "https://files.pythonhosted.org/packages/3f/05/42820ad63897bfd35cb7e591d79e8d21524c9da1520fa156b71d32f6953b/lxml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:d573b81c29e20b1513afa386a544797a99cecde5497e6c77b6dfa4484112c819", size = 3593261, upload-time = "2026-04-09T14:34:44.804Z" }, - { url = "https://files.pythonhosted.org/packages/e1/04/43c561e2293ede683f5259ceaccaf24ad6e830631123f197c1db483439ba/lxml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac63a1ef1899ccadace10ac937c41321672771378374c254e931d001448ae372", size = 4023698, upload-time = "2026-04-09T14:34:46.845Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5f/13fde57b45a0f88b8c4bb02156fb115e99ad48354029cb522b543f502563/lxml-6.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:10bc4f37c28b4e1b3e901dde66e3a096eb128acf388d5b2962dc2941284293bb", size = 3666947, upload-time = "2026-04-09T14:34:48.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4c/552571c619edd607432cbbf25e312a5d02859f2a7de421494a644b48451e/lxml-6.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ad6952810349cbfb843fe15e8afc580b2712359ae42b1d2b05d097bd48c4aea4", size = 8570109, upload-time = "2026-04-09T14:34:50.969Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/cf08843a6a923cd1eef40797a31e61424ac257c43634b5c9cff3bee93696/lxml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b81ec1ecac3be8c1ff1e00ca1c1baf8122e87db9000cd2549963847bd5e3b41", size = 4623404, upload-time = "2026-04-09T14:34:53.79Z" }, - { url = "https://files.pythonhosted.org/packages/b6/59/ffde0037a781b10c854abdf9e34fbf60d8f375ce8026551982b9f26695cc/lxml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:448e69211e59c39f398990753d15ba49f7218ec128f64ac8012ef16762e509a3", size = 4929662, upload-time = "2026-04-09T14:34:55.763Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/c468055e45954a93e1bc043a964d327d6784552d6551dc2364a1f83c53a1/lxml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6289cb9145fbbc5b0e159c9fcd7fc09446dadc6b60b72c4d1012e80c7c727970", size = 5092106, upload-time = "2026-04-09T14:34:58.522Z" }, - { url = "https://files.pythonhosted.org/packages/59/a3/8400c79a6defe609e24ce7b580f48d53f08acbf4c998eede0083a89f16f0/lxml-6.0.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b68c29aac4788438b07d768057836de47589c7deaa3ad8dc4af488dfc27be388", size = 5004214, upload-time = "2026-04-09T14:35:00.531Z" }, - { url = "https://files.pythonhosted.org/packages/57/b5/797246619cd0831c8d239f91fd4683683abbe7144854c6f33c68a6ea9f42/lxml-6.0.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50293e024afe5e2c25da2be68c8ceca8618912a0701a73f75b488317c8081aa6", size = 5630889, upload-time = "2026-04-09T14:35:02.89Z" }, - { url = "https://files.pythonhosted.org/packages/a0/fa/b86302385dc896d02ebb2803e4522a923acaa30e6cb35223492257ee24ab/lxml-6.0.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac65c08ba1bd90f662cb1d5c79f7ae4c53b1c100f0bb6ec5df1f40ac29028a7e", size = 5237728, upload-time = "2026-04-09T14:35:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/9b/7d/812c054b7d15f4dfb3a6fc877c2936023fcd8ac8b53807f996c8c60c4f57/lxml-6.0.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:16fbcf06ae534b2fa5bcdc19fcf6abd9df2e74fe8563147d1c5a687a130efed4", size = 5349527, upload-time = "2026-04-09T14:35:08.121Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4a/33a572874924809928747cd156b172b04cd19c1ec1d10925fc77dfeb676d/lxml-6.0.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3a0484bd1e84f82766befcbd71cccd7307dacfe08071e4dbc1d9a9b498d321e8", size = 4693177, upload-time = "2026-04-09T14:35:10.4Z" }, - { url = "https://files.pythonhosted.org/packages/36/d5/71842813ca0c43718f641e770195e278832f8c01870eaac857a3de34448a/lxml-6.0.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c137f8c8419c3de93e2998131d94628805f148e52b34da6d7533454e4d78bc2a", size = 5243928, upload-time = "2026-04-09T14:35:12.393Z" }, - { url = "https://files.pythonhosted.org/packages/da/a7/330845ae467c6086ef35977c335bb252fa11490082335f9ccfd0465bdfb7/lxml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:775266571f7027b1d77f5fce18a247b24f51a4404bdc1b90ec56be9b1e3801b9", size = 5046937, upload-time = "2026-04-09T14:35:15.209Z" }, - { url = "https://files.pythonhosted.org/packages/02/3d/b58b0aee0cf7e0b7eb5d24795a129c634c6d07f032d8b902bb0859319d13/lxml-6.0.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:aa18653b795d2c273b8676f7ad2ca916d846d15864e335f746658e4c28eb5168", size = 4776758, upload-time = "2026-04-09T14:35:17.758Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4c/f421b50f08c1b724a24c4a778db8888d0a2d948b4dd08b80f4f05a0804ff/lxml-6.0.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbffd22fc8e4d80454efa968b0c93440a00b8b8a817ce0c29d2c6cb5ad324362", size = 5644912, upload-time = "2026-04-09T14:35:20.438Z" }, - { url = "https://files.pythonhosted.org/packages/a7/99/eabfedb111ca1f26c8fe890413eabc7e2b0010f075fdf5bceb42737c3894/lxml-6.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7373ede7ccb89e6f6e39c1423b3a4d4ee48035d3b4619a6addced5c8b48d0ecc", size = 5233509, upload-time = "2026-04-09T14:35:23.137Z" }, - { url = "https://files.pythonhosted.org/packages/9f/17/050a105ca1154025b68c19901d45292cbdcee6f25bd056c178ad6b55e534/lxml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e759ff1b244725fef428c6b54f3dab4954c293b2d242a5f2e79db5cc3873de51", size = 5260150, upload-time = "2026-04-09T14:35:25.385Z" }, - { url = "https://files.pythonhosted.org/packages/61/a0/ed83517d12e9fe00101a21fe08a168fd69f57875d9416353e2a38c401df7/lxml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:f179bae37ad673f57756b59f26833b7922230bef471fdb29492428f152bae8c6", size = 3595160, upload-time = "2026-04-09T14:35:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/55/d3/101726831f45951fe3ddd03cffbd2a4ac6261fc63ada399e6f7051d43af6/lxml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:8eeec925ad7f81886d413b3a1f8715551f75543519229a9b35e957771e1826d5", size = 3996108, upload-time = "2026-04-09T14:35:29.608Z" }, - { url = "https://files.pythonhosted.org/packages/49/9f/ab1c58ad55bfcd4b55bafd98f19ff24f34315441f13aa787d5220def0702/lxml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:f96bba9a26a064ce9e11099bad12fb08384b64d3acc0acf94bf386ca5cf4f95f", size = 3658906, upload-time = "2026-04-09T14:35:32.451Z" }, - { url = "https://files.pythonhosted.org/packages/86/a6/2cdc9c5a634b1b890927f968febc2474fa3eb6fed99db82ea3c008bbbda4/lxml-6.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:83c1d75e9d124ab82a4ddaf59135112f0dc49526b47355e5928ae6126a68e236", size = 8559579, upload-time = "2026-04-09T14:35:35.644Z" }, - { url = "https://files.pythonhosted.org/packages/97/3c/adfbcdab17f89f72e069c5df5661c81e0511e3cdb353550f778e9ffaa08e/lxml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b683665d0287308adafc90a5617a51a508d8af8c7040693693bb333b5f4474fe", size = 4617332, upload-time = "2026-04-09T14:35:38.901Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d4/ee1a5c734a5ad79024fa85808f3efc18d5733813141e2bb2726a7d9d8bea/lxml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ed31e5852cd938704bc6c7a3822cbf84c7fa00ebfa914a1b4e2392d44f45bdfb", size = 4922821, upload-time = "2026-04-09T14:35:41.521Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1f/87efcc0b93ba4f95303ec8f80164f3c50db20a3a5612a285133f9ad6cb7e/lxml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8922a30704a4421d69a19e0499db5861da686c0bccc3a79cf3946e3155cf25f9", size = 5081226, upload-time = "2026-04-09T14:35:44.02Z" }, - { url = "https://files.pythonhosted.org/packages/65/8b/fd0fadd9ec8a6ac9d694014ccdb9504e28705abb2e08c9ca23c609020325/lxml-6.0.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a1adb0e220cb8691202ba9d97646a06292657a122df4b92733861d42f7cf4d2", size = 4992884, upload-time = "2026-04-09T14:35:46.769Z" }, - { url = "https://files.pythonhosted.org/packages/68/75/2fb0e534225214c6386496b7847195d7297b913cf563c5ccea394afc346b/lxml-6.0.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:821fd53699eb498990c915ba955a392d07246454c9405e6c1d0692362503013d", size = 5613383, upload-time = "2026-04-09T14:35:49.303Z" }, - { url = "https://files.pythonhosted.org/packages/54/3a/8f560f8fb2f5f092e18ac7a13a94b77e0e5213fe7c424d12e98393dcc7d8/lxml-6.0.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04b7cedf52e125f86d0d426635e7fbe8e353d4cc272a1757888e3c072424381d", size = 5228398, upload-time = "2026-04-09T14:35:51.611Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d5/6bf993c02a0173eb5883ace61958c55c245d3daf7753fb5f931a9691b440/lxml-6.0.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:9d98063e6ae0da5084ec46952bb0a5ccb5e2cad168e32b4d65d1ec84e4b4ebd4", size = 5342198, upload-time = "2026-04-09T14:35:54.311Z" }, - { url = "https://files.pythonhosted.org/packages/bb/18/637130349ca6aa33b6dc4796732835ede5017a811c5f55763a1c468f7971/lxml-6.0.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:ce01ab3449015358f766a1950b3d818eedf9d4cdec3fa87e4eecaad10c0784db", size = 4699178, upload-time = "2026-04-09T14:35:56.647Z" }, - { url = "https://files.pythonhosted.org/packages/bb/19/239daafcc1cfa42b8aa6384509a9fd2cb1aa281679c6e8395adf9ccbc189/lxml-6.0.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d38c25bad123d6ce30bb37931d90a4e8a167cd796eeae9cd16c2bfce52718f8e", size = 5231869, upload-time = "2026-04-09T14:36:00.41Z" }, - { url = "https://files.pythonhosted.org/packages/0a/74/db7fcadc651b988502bed00d48acfd8b997ecb5dd52ebcc05f39bf946d9e/lxml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9b8e0779780026979f217603385995202f364adc9807bd21210d81b9f562fc4e", size = 5043669, upload-time = "2026-04-09T14:36:02.463Z" }, - { url = "https://files.pythonhosted.org/packages/55/99/af795b579182fa04aa87fcb0bd112e22705d982f71eb53874a8d356b4091/lxml-6.0.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8c082ad2398664213a4bb5d133e2eb8bf239220b7d6688f8c8ffa9050057501f", size = 4769745, upload-time = "2026-04-09T14:36:04.716Z" }, - { url = "https://files.pythonhosted.org/packages/52/4d/10e652edc55d206188a1b738d1033aad3497886d34cb7f5fc753e67ecb49/lxml-6.0.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfc80c74233fe01157ab550fb12b9d07a2f1fa7c5900cefb484e3bf02e856fbc", size = 5635496, upload-time = "2026-04-09T14:36:06.815Z" }, - { url = "https://files.pythonhosted.org/packages/ab/68/95371835ec15bb46feee27b090bcabbe579f4ad04efbef08e2713bcfea16/lxml-6.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c45bdcdc2ca6cf26fddff3faa5de7a2ed7c7f6016b3de80125313a37f972378", size = 5223564, upload-time = "2026-04-09T14:36:09.057Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a6/0a9e5b63e8959487551be5d5496bb758ed2424c77ed7b25a9b8aae3b60c6/lxml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99457524afd384c330dc51e527976653d543ccadfa815d9f2d92c5911626e536", size = 5250124, upload-time = "2026-04-09T14:36:11.337Z" }, - { url = "https://files.pythonhosted.org/packages/d9/80/de3d3a790edf6d026c829fe8ccf54845058f57f8bb788e420c3b227eecef/lxml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:c8e3b8a54e65393ce1d5c7d9753fe756f0d96089e7163b20ddec3e5bb56a963e", size = 3596004, upload-time = "2026-04-09T14:36:13.446Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cf/43c9a5926060e39d99593921f37d7e88f129bc32ab6266b8460483abd613/lxml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:724b26a38cef98d6869d00a33cb66083bee967598e44f6a8e53f1dd283c851b0", size = 3994750, upload-time = "2026-04-09T14:36:15.686Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/b224dbc282bfef52d2e05645e405b5ed89c6391144dc09864229fe9ce88c/lxml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:f27373113fda6621e4201f529908a24c8a190c2af355aed4711dadca44db4673", size = 3657620, upload-time = "2026-04-09T14:36:17.952Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7c/3889981b55e83af1a710b2b54d40d5a9c7a2f7eab2e00cba6ba608fbdd22/lxml-6.0.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:093786037b934ef4747b0e8a0e1599fe7df7dd8246e7f07d43bba1c4c8bd7b84", size = 3929454, upload-time = "2026-04-09T14:38:54.873Z" }, - { url = "https://files.pythonhosted.org/packages/0b/29/a88dfb805c882b4fc81ef35d342629715a482037a0acd78ea8114e115d76/lxml-6.0.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6364aa77b13e04459df6a9d2b806465287e7540955527e75ebd5fda48532913d", size = 4209854, upload-time = "2026-04-09T14:38:57.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/01/44e71ace8c72bbb9aeb38551a4d314508133da88daf0dd9120a648af74ce/lxml-6.0.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:955550c78afb2be47755bd1b8153724292a5b539cf3f21665b310c145d08e6f8", size = 4317247, upload-time = "2026-04-09T14:38:59.977Z" }, - { url = "https://files.pythonhosted.org/packages/18/1a/ec02aafa56ff7675873e8fd4b6c7747aceaae037767434359e75d0b1075b/lxml-6.0.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9a79144a8051bc5fbb223fac895b87eb67b361f27b00c2ed4a07ee34246b90", size = 4250372, upload-time = "2026-04-09T14:39:02.289Z" }, - { url = "https://files.pythonhosted.org/packages/35/13/94acd22f85e34e22eb984b4ac3db4c1b0c1e3daa0433dac5053fd26954d8/lxml-6.0.3-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8243937d4673b46da90b4f5ea2627fd26842225e62e885828fdb8133aa1f7b32", size = 4401010, upload-time = "2026-04-09T14:39:04.598Z" }, - { url = "https://files.pythonhosted.org/packages/28/7a/b3e8ed85413a4bd5c4850dfbd1eb18be7428127be0986f2a679d9d6098ad/lxml-6.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5892d2ef99449ebd8e30544af5bc61fd9c30e9e989093a10589766422f6c5e1a", size = 3507669, upload-time = "2026-04-09T14:39:06.873Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, + { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, + { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, + { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, + { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, + { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, + { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, + { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, + { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, + { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, + { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, + { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, + { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, ] [[package]] name = "mako" -version = "1.3.10" +version = "1.3.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/8a/805404d0c0b9f3d7a326475ca008db57aea9c5c9f2e1e39ed0faa335571c/mako-1.3.11.tar.gz", hash = "sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069", size = 399811, upload-time = "2026-04-14T20:19:51.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://files.pythonhosted.org/packages/68/a5/19d7aaa7e433713ffe881df33705925a196afb9532efc8475d26593921a6/mako-1.3.11-py3-none-any.whl", hash = "sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77", size = 78503, upload-time = "2026-04-14T20:19:53.233Z" }, ] [[package]] @@ -3118,7 +3119,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.20.0" +version = "1.20.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -3126,30 +3127,30 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/1c/74cb1d9993236910286865679d1c616b136b2eae468493aa939431eda410/mypy-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4525e7010b1b38334516181c5b81e16180b8e149e6684cee5a727c78186b4e3b", size = 14343972, upload-time = "2026-03-31T16:49:04.887Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/01399515eca280386e308cf57901e68d3a52af18691941b773b3380c1df8/mypy-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a17c5d0bdcca61ce24a35beb828a2d0d323d3fcf387d7512206888c900193367", size = 13225007, upload-time = "2026-03-31T16:50:08.151Z" }, - { url = "https://files.pythonhosted.org/packages/56/ac/b4ba5094fb2d7fe9d2037cd8d18bbe02bcf68fd22ab9ff013f55e57ba095/mypy-1.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75ff57defcd0f1d6e006d721ccdec6c88d4f6a7816eb92f1c4890d979d9ee62", size = 13663752, upload-time = "2026-03-31T16:49:26.064Z" }, - { url = "https://files.pythonhosted.org/packages/db/a7/460678d3cf7da252d2288dad0c602294b6ec22a91932ec368cc11e44bb6e/mypy-1.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b503ab55a836136b619b5fc21c8803d810c5b87551af8600b72eecafb0059cb0", size = 14532265, upload-time = "2026-03-31T16:53:55.077Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3e/051cca8166cf0438ae3ea80e0e7c030d7a8ab98dffc93f80a1aa3f23c1a2/mypy-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1973868d2adbb4584a3835780b27436f06d1dc606af5be09f187aaa25be1070f", size = 14768476, upload-time = "2026-03-31T16:50:34.587Z" }, - { url = "https://files.pythonhosted.org/packages/be/66/8e02ec184f852ed5c4abb805583305db475930854e09964b55e107cdcbc4/mypy-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:2fcedb16d456106e545b2bfd7ef9d24e70b38ec252d2a629823a4d07ebcdb69e", size = 10818226, upload-time = "2026-03-31T16:53:15.624Z" }, - { url = "https://files.pythonhosted.org/packages/13/4b/383ad1924b28f41e4879a74151e7a5451123330d45652da359f9183bcd45/mypy-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:379edf079ce44ac8d2805bcf9b3dd7340d4f97aad3a5e0ebabbf9d125b84b442", size = 9750091, upload-time = "2026-03-31T16:54:12.162Z" }, - { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, - { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, - { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" }, - { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" }, - { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" }, - { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" }, - { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" }, - { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" }, - { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, ] [[package]] @@ -3402,7 +3403,7 @@ wheels = [ [[package]] name = "openai" -version = "2.31.0" +version = "2.32.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -3414,9 +3415,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/fe/64b3d035780b3188f86c4f6f1bc202e7bb74757ef028802112273b9dcacf/openai-2.31.0.tar.gz", hash = "sha256:43ca59a88fc973ad1848d86b98d7fac207e265ebbd1828b5e4bdfc85f79427a5", size = 684772, upload-time = "2026-04-08T21:01:41.797Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/bc/a8f7c3aa03452fedbb9af8be83e959adba96a6b4a35e416faffcc959c568/openai-2.31.0-py3-none-any.whl", hash = "sha256:44e1344d87e56a493d649b17e2fac519d1368cbb0745f59f1957c4c26de50a0a", size = 1153479, upload-time = "2026-04-08T21:01:39.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" }, ] [[package]] @@ -3885,11 +3886,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] [[package]] @@ -3965,11 +3966,11 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/17/9c3094b822982b9f1ea666d8580ce59000f61f87c1663556fb72031ad9ec/pathspec-1.1.0.tar.gz", hash = "sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080", size = 133918, upload-time = "2026-04-23T01:46:22.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/8eed0486f074e9f1ca7f8ce5ad663e65f12fdab344028d658fa1b03d35e0/pathspec-1.1.0-py3-none-any.whl", hash = "sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42", size = 56264, upload-time = "2026-04-23T01:46:20.606Z" }, ] [[package]] @@ -4160,7 +4161,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.5.1" +version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -4169,9 +4170,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] [[package]] @@ -4539,7 +4540,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -4547,9 +4548,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, ] [package.optional-dependencies] @@ -4559,85 +4560,88 @@ email = [ [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, ] [[package]] name = "pydantic-settings" -version = "2.13.1" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, ] [[package]] @@ -4735,40 +4739,40 @@ wheels = [ [[package]] name = "pypdf" -version = "6.9.2" +version = "6.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/83/691bdb309306232362503083cb15777491045dd54f45393a317dc7d8082f/pypdf-6.9.2.tar.gz", hash = "sha256:7f850faf2b0d4ab936582c05da32c52214c2b089d61a316627b5bfb5b0dab46c", size = 5311837, upload-time = "2026-03-23T14:53:27.983Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/3f/9f2167401c2e94833ca3b69535bad89e533b5de75fefe4197a2c224baec2/pypdf-6.10.2.tar.gz", hash = "sha256:7d09ce108eff6bf67465d461b6ef352dcb8d84f7a91befc02f904455c6eea11d", size = 5315679, upload-time = "2026-04-15T16:37:36.978Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/7e/c85f41243086a8fe5d1baeba527cb26a1918158a565932b41e0f7c0b32e9/pypdf-6.9.2-py3-none-any.whl", hash = "sha256:662cf29bcb419a36a1365232449624ab40b7c2d0cfc28e54f42eeecd1fd7e844", size = 333744, upload-time = "2026-03-23T14:53:26.573Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl", hash = "sha256:aa53be9826655b51c96741e5d7983ca224d898ac0a77896e64636810517624aa", size = 336308, upload-time = "2026-04-15T16:37:34.851Z" }, ] [[package]] name = "pypdfium2" -version = "5.7.0" +version = "5.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/76/19aacfff78d328a700ca34b5b1dff891e587aac2fd6b928b035ed366cc37/pypdfium2-5.7.0.tar.gz", hash = "sha256:9febb09f532555485f064c1f6442f46d31e27be5981359cb06b5826695906a06", size = 265935, upload-time = "2026-04-08T19:58:16.831Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/13/ee794b8a810b7226426c8b50d6c28637c059e7da0caf9936164f352ef858/pypdfium2-5.7.1.tar.gz", hash = "sha256:3b3b20a56048dbe3fd4bf397f9bec854c834668bc47ef6a7d9041b23bb04317b", size = 266791, upload-time = "2026-04-20T15:01:02.598Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/a5/7e6d9532e7753a1dc439412b38dda5943c692d3ab3f1e01826f9b5527c67/pypdfium2-5.7.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:9e815e75498a03a3049baf68ff00b90459bead0d9eee65b1860142529faba81d", size = 3343748, upload-time = "2026-04-08T19:57:40.293Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ea/9d4a0b41f86d342dfb6529c31789e70d1123cc6521b29979e02ec2b267b6/pypdfium2-5.7.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:405bb3c6d0e7a5a32e98eb45a3343da1ad847d6d6eef77bf6f285652a250e0b7", size = 2805480, upload-time = "2026-04-08T19:57:42.109Z" }, - { url = "https://files.pythonhosted.org/packages/34/dc/ce1c8e94082a84d1669606f90c4f694acbdcabd359d92db7302d16b5938b/pypdfium2-5.7.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:609b34d91871c185f399b1a503513c03a9de83597f55404de00c3d31a8037544", size = 3420156, upload-time = "2026-04-08T19:57:43.672Z" }, - { url = "https://files.pythonhosted.org/packages/51/84/6d859ce82a3723ba7cd70d88ad87eca3cb40553c68db182976fd2b0febe1/pypdfium2-5.7.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:6ae6c6bba0cde30c9293c3f525778c229466de7782e8f7d99e7c2a1b8f9c7a6f", size = 3601560, upload-time = "2026-04-08T19:57:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/66/0c/8bc2258d1e7ba971d05241a049cd3100c75df6bcf930423de7d0c6265a30/pypdfium2-5.7.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b518d78211cb2912139d10d7f4e39669231eb155e8258159e3413e9e5e4baef", size = 3588134, upload-time = "2026-04-08T19:57:47.379Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/3248cc569a92ff25f1fe0a4a1790807e6e05df60563e39e74c9b723d5620/pypdfium2-5.7.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aaa8e7681ebcaa042ac8adc152521fd5f16a4ceee1e9b9b582e148519528aa9", size = 3323100, upload-time = "2026-04-08T19:57:49.243Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ee/6f004509df77ce963ed5a0f2e090ea0c43036e49cc72c321ce90f3d328bf/pypdfium2-5.7.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d2284f799adbae755b66ce1a579834e487337d89bbb34ee749ecfa68322425", size = 3719217, upload-time = "2026-04-08T19:57:50.708Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f0/bb61601aa1c2990d4a5d194440281941781250f6a438813a13fe20eb95cf/pypdfium2-5.7.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08e9e9576eefbc085ba9a63feede4bcaf93d9fa0d9b17cb549aba6f065a8750e", size = 4147676, upload-time = "2026-04-08T19:57:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/bd/27/a119e0519049afcfca51e9834b67949ffaba5b9afe7e74ed04d6c39b0285/pypdfium2-5.7.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ace647320bae562903097977b83449f91d30e045dd19ce62939d3100869f180", size = 3635469, upload-time = "2026-04-08T19:57:53.948Z" }, - { url = "https://files.pythonhosted.org/packages/70/0b/4bcb67b039f057aca01ddbe692ae7666b630ad42b91a3aca3cb4d4f01222/pypdfium2-5.7.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7bb7555fe613cd76fff871a12299f902b80443f90b49e2001338718c758f6f4", size = 3091818, upload-time = "2026-04-08T19:57:55.471Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c9/31490ab7cecaf433195683ff5c750f4111c7347f1fef9131d3d8704618eb/pypdfium2-5.7.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7c0ef5ae35d40daa1883f3993b3b7ecf3fb06993bcc46651e28cf058d9da992", size = 2959579, upload-time = "2026-04-08T19:57:57.238Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/bf5fe52f007130c0b1b38786ef82c98b4ac06f77e7ca001a17cda6ce76b6/pypdfium2-5.7.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:423c749e8cab22ddaf833041498ec5ad477c1c2abbff0a8ec00b99663c284592", size = 4126033, upload-time = "2026-04-08T19:57:59.111Z" }, - { url = "https://files.pythonhosted.org/packages/18/7d/46dcebf4eb9ccf9b5fafe79702c31863b4c127e9c3140c0f335c375d3818/pypdfium2-5.7.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f48f453f848a90ec7786bcc84a4c0ee42eb84c2d8af3ca9004f7c18648939838", size = 3742063, upload-time = "2026-04-08T19:58:00.643Z" }, - { url = "https://files.pythonhosted.org/packages/4d/29/cfec37942f13a1dfe3ab059cf8d130609143d33ca1dd554b017a30bffe97/pypdfium2-5.7.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e84bfa61f0243ed4b33bfe2492946ba761007b7feb5e7e0a086c635436d47906", size = 4332177, upload-time = "2026-04-08T19:58:02.425Z" }, - { url = "https://files.pythonhosted.org/packages/3f/da/07812153eff746bbc548d50129ada699765036674ff94065d538015c9556/pypdfium2-5.7.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:e3f4d7f4473b5ef762560cd5971cad3b51a77da3a25af479ef5aae4611709bb8", size = 4370704, upload-time = "2026-04-08T19:58:04.379Z" }, - { url = "https://files.pythonhosted.org/packages/9b/df/07a6a038ccb6fae6a1a06708c98d00aa03f2ca720b02cd3b75248dc5da70/pypdfium2-5.7.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:9e0b6c9be8c92b63ce0a00a94f6635eec22831e253811d6692824a1244e21780", size = 3924428, upload-time = "2026-04-08T19:58:06.406Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a8/70ce4f997fef4186098c032fb3dd2c39193027a92a23b5a94d7a4c85e068/pypdfium2-5.7.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:3e4974a8545f726fc97a7443507713007e177f22058cd1ca0b28cb0e8e2d7dc2", size = 4264817, upload-time = "2026-04-08T19:58:08.003Z" }, - { url = "https://files.pythonhosted.org/packages/02/42/03779e61ca40120f87839b4693899c72031b7a9e23676dcd8914d92e460c/pypdfium2-5.7.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2fe12d57a0b413d42bdba435a608b2435a921a5f6a9d78fd8091b6266b63901a", size = 4175393, upload-time = "2026-04-08T19:58:09.858Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f1/19bea36b354f2407c6ffdc60ad8564d95eb515badec457043ff57ad636f0/pypdfium2-5.7.0-py3-none-win32.whl", hash = "sha256:23958aec5c28c52e71f183a647fcc9fcec96ef703cc60a3ade44e55f4701678f", size = 3606308, upload-time = "2026-04-08T19:58:11.672Z" }, - { url = "https://files.pythonhosted.org/packages/70/aa/fb333c1912a019de26e2395afd3dbef09e8118a59d70f1e5886fc90aa565/pypdfium2-5.7.0-py3-none-win_amd64.whl", hash = "sha256:a33d2c190042ae09c5512f599a540f88b07be956f18c4bb49c027e8c5118ce44", size = 3726429, upload-time = "2026-04-08T19:58:13.374Z" }, - { url = "https://files.pythonhosted.org/packages/86/cf/6d4bc1ae4466a1f223abfe27210dce218da307e921961cd687f6e5a795a0/pypdfium2-5.7.0-py3-none-win_arm64.whl", hash = "sha256:8233fd06b0b8c22a5ea0bccbd7c4f73d6e9d0388040ea51909a5b2b1f63157e8", size = 3519317, upload-time = "2026-04-08T19:58:15.261Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f7/e87ba0eec9cd4e9eedd4bbb867515da970525ca8c105dd5e254758216ee3/pypdfium2-5.7.1-py3-none-android_23_arm64_v8a.whl", hash = "sha256:8008f45e8adc4fc1ec2a51e018e01cd0692d4859bdbb28e88be221804f329468", size = 3367033, upload-time = "2026-04-20T15:00:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e1/a4b9be9a09fa9857958357ced51afb25518f6a48e4e68fdc9a091f0f2259/pypdfium2-5.7.1-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:892fcb5a618f5f551fffdb968ac2d64911953c3ba0f9aa628239705af68dbe15", size = 2824449, upload-time = "2026-04-20T15:00:24.913Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5d/c91abb2610316a1622f86ddf706fcd04d34c7e6923c3fa8fa145c8f7a372/pypdfium2-5.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7431847d45dedc3c7ffede15b58ac611e996a0cdcd61318a0190d46b9980ac2b", size = 3443730, upload-time = "2026-04-20T15:00:26.664Z" }, + { url = "https://files.pythonhosted.org/packages/50/8b/b9eefed83d6a0a59384ee64d25c1515e831c234c3ed6b8c6dfc8f99f4875/pypdfium2-5.7.1-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:548bd09c9f97565ae8ddba30bb65823cbf791b84e4cdb63ed582aec2c289dbe2", size = 3626483, upload-time = "2026-04-20T15:00:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/5b/98/6d62723e1f58d66e7e0073c4f12048f9d5dcd478369da0990db08e677dd5/pypdfium2-5.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18a15ad0918acc3ea98778394f0331b9ad2a1b7384ab3d8d8c63422ffd01ed13", size = 3610098, upload-time = "2026-04-20T15:00:30.344Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4a/f72b42578f30971c29915e33ee598ed451aa6f0c2808a71526c1b81afd8d/pypdfium2-5.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1df04564659d807fb38810d9bd1ac18419d8acbb5f87f2cb20675d7332635b18", size = 3340119, upload-time = "2026-04-20T15:00:32.19Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/de69c5feed470617f243e61cac841bfd1b5273d575c3d3b49b27f738e334/pypdfium2-5.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a146d036a6b085a406aa256548b827b63016714fd77f8e11b7f704c1175e8cc", size = 3738864, upload-time = "2026-04-20T15:00:33.798Z" }, + { url = "https://files.pythonhosted.org/packages/07/ce/69ff10766565c5ffcb66cebe780ce3bc4fe7cc16b218df8c240075881c66/pypdfium2-5.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3397b0d705b6858c87dec1dc9c44d4c7094601a9b231097f441b64d1a7d5ff0b", size = 4169839, upload-time = "2026-04-20T15:00:35.973Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/fff16a831a6f07aad02da0d02b620c455310b8bf4e2642909175dcb7ccae/pypdfium2-5.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc2cdf603ac766b91b7c1b455197ec1c3471089d75f999b046edb65ed6cedd80", size = 3657630, upload-time = "2026-04-20T15:00:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/9b/58/d3148917616164cfad347b0b509342737ed80e060afab07523ffeac2a05f/pypdfium2-5.7.1-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b1a6a5f3320b59138e7570a3f78840540383d058ac180a9a21f924ad3bd7f83", size = 3088898, upload-time = "2026-04-20T15:00:40.109Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/387ca4dfe9865a8d61114dae2debba4d86eed07cdc6a31c5527a049583be/pypdfium2-5.7.1-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:91b809c40a5fc248107d13fbcf1dd2c64dbc8e572693a9b93e350bf31efda92b", size = 2955404, upload-time = "2026-04-20T15:00:41.921Z" }, + { url = "https://files.pythonhosted.org/packages/ad/87/4afc2bfe35d71942f1bf9e774086f74af66a0a4e56338f39a7cbc5b8721c/pypdfium2-5.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85611ef61cbc0f5e04de8f99fec0f3db3920b09f46c62afa08c9caa21a74b353", size = 4126600, upload-time = "2026-04-20T15:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/872eef4cb8f0d8ebbf967ca713254ac71c75878a1d5798bc2b8d23104e52/pypdfium2-5.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b2764ab909f9b444d4e643be90b064c4053e6828c28bfd47639fc84526ba244d", size = 3742636, upload-time = "2026-04-20T15:00:46.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/3805a53623a72e20b68e6814b37582994298b231628656ff227fa1158a1f/pypdfium2-5.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:fcea3cc20b7cca7d84ceee68b9c6ef7fe773fb71c145542769dc2ceb27e9698a", size = 4332743, upload-time = "2026-04-20T15:00:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/92/61/3e3f8ae7ad04400bc3c6a75bbf59db500eaf9dff05477d1b25ff4a36363b/pypdfium2-5.7.1-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:f04546bc314973397148805d44f8e660e81aa80c2a87e12afb892c11493ded6c", size = 4377471, upload-time = "2026-04-20T15:00:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e0/1026f297b5be292cae7095aa4814d57faa3faba0b49552afcaa11a1c2e4e/pypdfium2-5.7.1-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:66275c8a854969bdf905abc7599e5623d62739c44604d69788ff5457082d275b", size = 3919215, upload-time = "2026-04-20T15:00:51.2Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/7d6d5b392fa42a997aadf127e3b2c25739199141054b33f759ba5d02e653/pypdfium2-5.7.1-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:bbed8f32040ce3b3236a512265976017c2465ea6643a1730f008b39e0339b8ce", size = 4263089, upload-time = "2026-04-20T15:00:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b8/d51bd4a1d426fa5b99d4516c77cc1892a8fbfd5a93a823e2679cf9b09ee0/pypdfium2-5.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c55d3df09bd0d72a1d192107dcbf80bcb2791662a3eca3b084001f947d3040d5", size = 4175967, upload-time = "2026-04-20T15:00:54.757Z" }, + { url = "https://files.pythonhosted.org/packages/30/52/06a6358856374ae4400ee1ad0ddaa01d5c31fcd6e8f4577e6a3ed1c40343/pypdfium2-5.7.1-py3-none-win32.whl", hash = "sha256:4f6bbe1211c5883c8fc9ce11008347e5b96ec6571456d959ae289cecdb2867f0", size = 3629154, upload-time = "2026-04-20T15:00:56.916Z" }, + { url = "https://files.pythonhosted.org/packages/6f/13/e0dbc9377d976d8b03ed0dd07fe9892e06d09fcf4f6a0e66df49366227d7/pypdfium2-5.7.1-py3-none-win_amd64.whl", hash = "sha256:fdf117af26bd310f4f176b3cf0e2e23f0f800e48dcf2bcf6c2cca0de3326f5cb", size = 3747295, upload-time = "2026-04-20T15:00:59.15Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/4759522f5bca0ac4cda9f42c7f3f818aa826568793bd8b4532d2d2ffa515/pypdfium2-5.7.1-py3-none-win_arm64.whl", hash = "sha256:622821698fcc30fc560bd4eead6df9e6b846de9876b82861bed0091c09a4c27b", size = 3540903, upload-time = "2026-04-20T15:01:00.994Z" }, ] [[package]] @@ -4981,11 +4985,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.24" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/45/e23b5dc14ddb9918ae4a625379506b17b6f8fc56ca1d82db62462f59aea6/python_multipart-0.0.24.tar.gz", hash = "sha256:9574c97e1c026e00bc30340ef7c7d76739512ab4dfd428fec8c330fa6a5cc3c8", size = 37695, upload-time = "2026-04-05T20:49:13.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/73/89930efabd4da63cea44a3f438aeb753d600123570e6d6264e763617a9ce/python_multipart-0.0.24-py3-none-any.whl", hash = "sha256:9b110a98db707df01a53c194f0af075e736a770dc5058089650d70b4a182f950", size = 24420, upload-time = "2026-04-05T20:49:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -5495,27 +5499,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" +version = "0.15.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] @@ -6030,7 +6034,7 @@ wheels = [ [[package]] name = "temporalio" -version = "1.25.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nexus-rpc" }, @@ -6038,13 +6042,13 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/d4/fa21150a225393f87732ed6fef3cc9735d9e751edc6be415fe6e375105c6/temporalio-1.26.0.tar.gz", hash = "sha256:f4bfb35125e6f5e8c7f7ed1277c7354d812c6fac7ed5f8dbd50536cf289aaaa7", size = 2388994, upload-time = "2026-04-15T23:43:00.911Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/1e/27/8c421c622d18cc8e034247d5d72b89e6456937344b5bec1de40abef3c085/temporalio-1.26.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5489040c0cf621edeb36984199dd9e4fbd2b3a07d61a4f2a8da1f2cb9820ef26", size = 14221070, upload-time = "2026-04-15T23:42:26.21Z" }, + { url = "https://files.pythonhosted.org/packages/49/7c/d2b691d16ec5db87198c2e08dbfba58e286c096faee15753613a581abdce/temporalio-1.26.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b18dd85771509c19ef059a31908bcd4e6130d1f67037c4db519702f3f2ad6d4a", size = 13583991, upload-time = "2026-04-15T23:42:34.357Z" }, + { url = "https://files.pythonhosted.org/packages/05/ca/b8728451320ca9d8bb6e1680b9bd23767118f86d5b8644edf2304d533f1b/temporalio-1.26.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46187d5f82ca2ae81f35ea5916a76db0e2f067210dc6b1852c3749475721946e", size = 13808036, upload-time = "2026-04-15T23:42:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/cb/54/3113f5e0ac58655790abac64656373e06191b351d74bfb94692e81bd6784/temporalio-1.26.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03300c3e5237443367ac61bb20bd726c656b3daa50310bdd436599d5bdc7cf97", size = 14336604, upload-time = "2026-04-15T23:42:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9b/c50840a26af3587c0c8d9af04d9976743e22496996dc1a377efc75dcd316/temporalio-1.26.0-cp310-abi3-win_amd64.whl", hash = "sha256:1c4a0d82f0a3796cbf78864c799f8dca0b94cdaec68e7b8b224c859005686ec4", size = 14525849, upload-time = "2026-04-15T23:42:57.589Z" }, ] [package.optional-dependencies] @@ -6238,7 +6242,7 @@ wheels = [ [[package]] name = "typer" -version = "0.24.1" +version = "0.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -6246,9 +6250,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" }, ] [[package]] @@ -6262,16 +6266,16 @@ wheels = [ [[package]] name = "types-boto3" -version = "1.42.87" +version = "1.42.95" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/60/e7a90772aa8fe57391a64154e6b1ab626f611b34d2a8a57432ce28bbc38b/types_boto3-1.42.87.tar.gz", hash = "sha256:b59318f1d4954e785d3f5fbf1fc29e84c296ee1720a9ae5bb8afdd2d3e124eb3", size = 102851, upload-time = "2026-04-09T19:58:21.238Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/e6/edde18693736fd6d3c200ff03394e8b3eec3cedcae36eb3ae3e2a39428af/types_boto3-1.42.95.tar.gz", hash = "sha256:bce21bb72d21d80e59d5ce18a1eecb57f23fdca49e516190e81f305b3bf60401", size = 102975, upload-time = "2026-04-23T21:37:21.239Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/85/45abcb9dc314d7b09fbc40425840781f05df74bf55ecb239048ba6dc1a75/types_boto3-1.42.87-py3-none-any.whl", hash = "sha256:ded49d50f097707e6c876ee280cbd21919868a74d6aafc7919ca6b2e5a1272e9", size = 70510, upload-time = "2026-04-09T19:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/80/8c/f5e43d8a19e370f0a3f111084c65f12dc8042162049749d75dd572c17fb4/types_boto3-1.42.95-py3-none-any.whl", hash = "sha256:ad5c026ac3378fc4d74ef3bbe4b63fab6ae821cf4e5718a035cfa80224d1f3fe", size = 70560, upload-time = "2026-04-23T21:37:16.898Z" }, ] [package.optional-dependencies] @@ -6281,14 +6285,14 @@ full = [ [[package]] name = "types-boto3-full" -version = "1.42.86" +version = "1.42.94" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/bd/90f2b201c37520cd1206fa34347e141e2117dbf11c01e8357ca9e3dc7d5f/types_boto3_full-1.42.86.tar.gz", hash = "sha256:f2fe60d3067e3004fd44743c62ffb82d97e69239ab3d7e609d9712f213fcd457", size = 8635402, upload-time = "2026-04-09T01:46:35.239Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/7e/2f301b5f868396524b01d79a997b7a1354ca134b75b517b7b85d6109e216/types_boto3_full-1.42.94.tar.gz", hash = "sha256:e665376c15117f9cbcc21c89a21c2449bd3f4e6f4fdba2d7c9759d3a8f99f4ee", size = 8692455, upload-time = "2026-04-23T02:04:07.677Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/c2/1f6a59d50cb7dbea4eeff7b9641ec3c77b8106b3e4295e257dd7195858ff/types_boto3_full-1.42.86-py3-none-any.whl", hash = "sha256:1a28892bd19a6466d7b548b090755647d59aaf01d5232ef7ba2912d31f23a5fb", size = 13120764, upload-time = "2026-04-09T01:46:31.017Z" }, + { url = "https://files.pythonhosted.org/packages/d4/08/633dce6de18f5e0428ec46d376e771655fa3ed56518ebb25c6f4858c76ef/types_boto3_full-1.42.94-py3-none-any.whl", hash = "sha256:77afb5d04e678e7298a1a488b54c9b95a3d81f73e6ebe4d832f322fd8a46d160", size = 13192128, upload-time = "2026-04-23T02:04:04.328Z" }, ] [[package]] @@ -6389,15 +6393,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.44.0" +version = "0.46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, + { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, ] [package.optional-dependencies] @@ -6439,7 +6443,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.2.1" +version = "21.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -6447,9 +6451,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/c5/aff062c66b42e2183201a7ace10c6b2e959a9a16525c8e8ca8e59410d27a/virtualenv-21.2.1.tar.gz", hash = "sha256:b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78", size = 5844770, upload-time = "2026-04-09T18:47:11.482Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/0e/f083a76cb590e60dff3868779558eefefb8dfb7c9ed020babc7aa014ccbf/virtualenv-21.2.1-py3-none-any.whl", hash = "sha256:bd16b49c53562b28cf1a3ad2f36edb805ad71301dee70ddc449e5c88a9f919a2", size = 5828326, upload-time = "2026-04-09T18:47:09.331Z" }, + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, ] [[package]] @@ -6772,11 +6776,11 @@ wheels = [ [[package]] name = "zipp" -version = "3.23.0" +version = "3.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, ] [[package]]