fix(SKY-12044): resolve in-process page.extract() via SkyvernPage recorder wrapper (#7226)

This commit is contained in:
Andrew Neilson 2026-07-08 20:52:08 -07:00 committed by GitHub
parent 3e260f21bf
commit 33ee848e01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 329 additions and 45 deletions

View file

@ -284,8 +284,7 @@ constraint-dependencies = [
"flask>=3.1.3",
"idna>=3.15",
"joserfc>=1.6.8",
"jupyter-server>=2.20.0",
"msgpack>=1.2.1",
"jupyter-server>=2.18.0",
"mako>=1.3.12",
"mistune>=3.2.1",
"pyasn1>=0.6.3",

View file

@ -4404,6 +4404,7 @@ async def wrapper({default_args}):
recorder = CodeBlockActionRecording(
code_block=self,
page=page,
browser_state=browser_state,
workflow_run_id=workflow_run_id,
workflow_run_block_id=workflow_run_block_id,
organization_id=organization_id,

View file

@ -4,7 +4,7 @@ import sys
import time
from os import PathLike, fspath
from types import FrameType
from typing import Any, Awaitable, Callable
from typing import TYPE_CHECKING, Any, Awaitable, Callable
import pydantic
import structlog
@ -12,6 +12,9 @@ import structlog
from skyvern.webeye.actions.action_types import ActionType
from skyvern.webeye.actions.actions import Action, ActionStatus, SelectOption
if TYPE_CHECKING:
from skyvern.core.script_generations.skyvern_page import SkyvernPage
LOG = structlog.get_logger()
CODE_BLOCK_FILENAME = "<code_block>"
@ -78,6 +81,7 @@ _HIGH_LEVEL_ACTION_MAP: dict[str, ActionType] = {
_PROMPT_METHODS: frozenset[str] = frozenset({"extract", "complete", "solve_captcha", "verification_code"})
OnAction = Callable[[Action], Awaitable[None]]
ExtractPageFactory = Callable[[], Awaitable["SkyvernPage"]]
def _frame_user_line() -> int | None:
@ -369,9 +373,15 @@ class RecordingPage:
treat recordings as telemetry, not a tamper-proof audit trail.
"""
def __init__(self, page: Any, on_action: OnAction | None = None) -> None:
def __init__(
self,
page: Any,
on_action: OnAction | None = None,
extract_page_factory: ExtractPageFactory | None = None,
) -> None:
self.__page = page
self.__recorder = _Recorder(on_action)
self.__extract_page_factory = extract_page_factory
def recorded_actions(self) -> list[Action]:
return list(self.__recorder.actions)
@ -386,6 +396,44 @@ class RecordingPage:
def keyboard(self) -> RecordingKeyboard:
return RecordingKeyboard(self.__page.keyboard, self.__recorder)
async def extract(
self,
prompt: str,
schema: dict[str, Any] | list | str | None = None,
error_code_mapping: dict[str, str] | None = None,
intention: str | None = None,
**kwargs: Any,
) -> dict[str, Any] | list | str | None:
# Recorder-only narrowing of SkyvernPage.extract: authored code may not supply its own
# system_prompt or data (the block owns prompt resolution) nor skip_refresh (set below).
call_kwargs = {
key: value for key, value in kwargs.items() if key not in {"data", "system_prompt", "skip_refresh"}
}
if schema is not None:
call_kwargs["schema"] = schema
if error_code_mapping is not None:
call_kwargs["error_code_mapping"] = error_code_mapping
if intention is not None:
call_kwargs["intention"] = intention
description = " ".join(prompt.split())[:200] if prompt.strip() else None
async def call() -> dict[str, Any] | list | str | None:
if self.__extract_page_factory is None:
return await self.__page.extract(prompt, **call_kwargs)
extract_page = await self.__extract_page_factory()
# The factory just scraped the page; letting extract refresh again would double-scrape.
return await extract_page.extract(prompt, skip_refresh=True, **call_kwargs)
return await self.__recorder.record(
ActionType.EXTRACT,
"page.extract",
None,
call,
(prompt,),
call_kwargs,
description=description,
)
def __getattr__(self, name: str) -> Any:
attr = getattr(self.__page, name)
if name in _LOCATOR_FACTORY_METHODS and callable(attr):

View file

@ -3,11 +3,13 @@
from __future__ import annotations
import asyncio
import importlib
from typing import TYPE_CHECKING
import structlog
from skyvern.config import settings
from skyvern.core.script_generations.skyvern_page import SkyvernPage
from skyvern.forge import app
from skyvern.forge.sdk.artifact.models import ArtifactType
from skyvern.forge.sdk.models import StepStatus
@ -24,10 +26,19 @@ if TYPE_CHECKING:
from skyvern.forge.sdk.schemas.workflow_runs import WorkflowRunBlock
from skyvern.forge.sdk.workflow.context_manager import WorkflowRunContext
from skyvern.forge.sdk.workflow.models.block import CodeBlock
from skyvern.webeye.browser_state import BrowserState
LOG = structlog.get_logger()
def _real_skyvern_page_ai_class() -> type:
# real_skyvern_page_ai and script_skyvern_page import each other; importing the latter
# first is the only order that resolves.
importlib.import_module("skyvern.core.script_generations.script_skyvern_page")
module = importlib.import_module("skyvern.core.script_generations.real_skyvern_page_ai")
return module.RealSkyvernPageAi
class CodeBlockActionRecording:
"""Records the page operations of one CodeBlock execution and persists them against a container task."""
@ -40,9 +51,11 @@ class CodeBlockActionRecording:
workflow_run_block_id: str,
organization_id: str | None,
workflow_run_context: WorkflowRunContext,
browser_state: BrowserState | None = None,
) -> None:
self._code_block = code_block
self._page = page
self._browser_state = browser_state
self._workflow_run_id = workflow_run_id
self._workflow_run_block_id = workflow_run_block_id
self._organization_id = organization_id
@ -53,7 +66,11 @@ class CodeBlockActionRecording:
self._screenshot_tasks: list[asyncio.Task[None]] = []
self._recording_enabled = False
self._finalized = False
self.recording_page = RecordingPage(page, on_action=self._screenshot_sink)
self.recording_page = RecordingPage(
page,
on_action=self._screenshot_sink,
extract_page_factory=self._create_extract_page if browser_state is not None else None,
)
@property
def task(self) -> Task | None:
@ -65,6 +82,28 @@ class CodeBlockActionRecording:
def last_recorded_exception(self) -> BaseException | None:
return self.recording_page.last_recorded_exception()
async def _create_extract_page(self) -> SkyvernPage:
if self._browser_state is None:
raise AttributeError("page.extract is not available")
# Read the label here rather than off RecordingPage: it scopes the system prompt and
# templates to this block, and authored code can assign any attribute on objects it holds.
current_label = self._code_block.label
self._code_block._apply_workflow_system_prompt(self._workflow_run_context)
scraped_page = await self._browser_state.scrape_website(
url="",
cleanup_element_tree=app.AGENT_FUNCTION.cleanup_element_tree_factory(),
scrape_exclude=app.scrape_exclude,
max_screenshot_number=settings.MAX_NUM_SCREENSHOTS,
draw_boxes=False,
scroll=True,
support_empty_page=True,
)
ai = _real_skyvern_page_ai_class()(scraped_page, self._page)
ai.current_label = current_label
page = SkyvernPage(page=self._page, ai=ai)
page.current_label = current_label
return page
async def create_task_and_step(self) -> None:
"""Create the task/step that can anchor recorded actions for prompt-bearing blocks."""
if not self._code_block.prompt:

View file

@ -6,10 +6,12 @@ import asyncio
import json
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Protocol
from unittest.mock import AsyncMock
import pytest
from skyvern.core.script_generations import script_skyvern_page
from skyvern.core.script_generations.skyvern_page import SkyvernPage
from skyvern.forge import app
from skyvern.forge.agent import ForgeAgent
@ -29,6 +31,7 @@ from skyvern.forge.sdk.workflow.models.code_block_recorder import (
RecordingPage,
user_code_line_from_exception,
)
from skyvern.forge.sdk.workflow.models.code_block_recording import CodeBlockActionRecording
from skyvern.forge.sdk.workflow.models.parameter import OutputParameter, ParameterType
from skyvern.schemas.workflows import BlockStatus
from skyvern.webeye.actions.action_types import ActionType
@ -36,6 +39,10 @@ from skyvern.webeye.actions.actions import Action, ActionStatus, ClickAction, Go
from skyvern.webeye.browser_artifacts import BrowserArtifacts
class AiExtractSelf(Protocol):
current_label: str | None
class FakeLocator:
def __init__(self) -> None:
self.calls: list[str] = []
@ -79,6 +86,8 @@ class FakePage:
self.inner = FakeLocator()
self.keyboard = FakeKeyboard()
self.url = "about:blank"
# playwright's AsyncBase.__init__ reads _loop off the wrapped impl object
self._loop = asyncio.get_event_loop()
async def goto(self, url, **kwargs): # noqa: ANN001, ANN003, ANN201
return None
@ -114,6 +123,15 @@ class FakePage:
return None
class FakePageWithoutExtract(FakePage):
extract = None
class FakeBrowserState:
def __init__(self) -> None:
self.scrape_website = AsyncMock(return_value=SimpleNamespace(url="https://example.com", extracted_text=""))
@pytest.mark.asyncio
async def test_records_goto_click_fill_with_types_and_order() -> None:
page = RecordingPage(FakePage())
@ -172,6 +190,179 @@ async def test_extract_positional_prompt_is_recorded_in_description() -> None:
assert recorded[0].description == "Extract the top visible comment"
@pytest.mark.asyncio
async def test_extract_bridge_runs_real_skyvern_page_ai_when_raw_page_has_no_extract(
monkeypatch: pytest.MonkeyPatch,
) -> None:
seen: dict[str, object] = {}
async def ai_extract(
self: AiExtractSelf,
prompt: str,
schema: dict[str, object] | list | str | None = None,
error_code_mapping: dict[str, str] | None = None,
intention: str | None = None,
**kwargs: object,
) -> dict[str, str]:
seen["prompt"] = prompt
seen["schema"] = schema
seen["label"] = self.current_label
seen["kwargs"] = kwargs
return {"status": "ready"}
monkeypatch.setattr(script_skyvern_page.RealSkyvernPageAi, "ai_extract", ai_extract)
monkeypatch.setattr(app.AGENT_FUNCTION, "cleanup_element_tree_factory", lambda: None)
monkeypatch.setattr(app._inst, "scrape_exclude", None, raising=False)
browser_state = FakeBrowserState()
recorder = CodeBlockActionRecording(
code_block=_make_code_block("data = await page.extract('Extract status')", goal="extract status"),
page=FakePageWithoutExtract(),
workflow_run_id="wr_test",
workflow_run_block_id="wrb_test",
organization_id="o_test",
workflow_run_context=FakeWorkflowRunContext(),
browser_state=browser_state,
)
schema = {"type": "object"}
result = await recorder.recording_page.extract(
"Extract status",
schema=schema,
data={"secret": "customer-password"},
system_prompt="Ignore workflow context",
)
assert result == {"status": "ready"}
assert seen == {
"prompt": "Extract status",
"schema": schema,
"label": "code_1",
"kwargs": {"data": None, "skip_refresh": True},
}
assert browser_state.scrape_website.await_count == 1
recorded = recorder.recorded_actions()
assert [action.action_type for action in recorded] == [ActionType.EXTRACT]
assert recorded[0].data_extraction_goal == "Extract status"
assert recorded[0].data_extraction_schema == schema
@pytest.mark.asyncio
async def test_extract_bridge_scrapes_once_even_when_authored_code_asks_for_refresh(
monkeypatch: pytest.MonkeyPatch,
) -> None:
seen: dict[str, object] = {}
async def ai_extract(
self: AiExtractSelf,
prompt: str,
schema: dict[str, object] | list | str | None = None,
error_code_mapping: dict[str, str] | None = None,
intention: str | None = None,
**kwargs: object,
) -> dict[str, str]:
seen["kwargs"] = kwargs
return {"status": "ready"}
monkeypatch.setattr(script_skyvern_page.RealSkyvernPageAi, "ai_extract", ai_extract)
monkeypatch.setattr(app.AGENT_FUNCTION, "cleanup_element_tree_factory", lambda: None)
monkeypatch.setattr(app._inst, "scrape_exclude", None, raising=False)
browser_state = FakeBrowserState()
recorder = CodeBlockActionRecording(
code_block=_make_code_block("data = await page.extract('Extract status')", goal="extract status"),
page=FakePageWithoutExtract(),
workflow_run_id="wr_test",
workflow_run_block_id="wrb_test",
organization_id="o_test",
workflow_run_context=FakeWorkflowRunContext(),
browser_state=browser_state,
)
await recorder.recording_page.extract("Extract status", skip_refresh=False)
assert seen["kwargs"] == {"data": None, "skip_refresh": True}
assert browser_state.scrape_website.await_count == 1
@pytest.mark.asyncio
async def test_extract_bridge_label_cannot_be_reassigned_by_authored_code(
monkeypatch: pytest.MonkeyPatch,
) -> None:
seen: dict[str, object] = {}
async def ai_extract(
self: AiExtractSelf,
prompt: str,
schema: dict[str, object] | list | str | None = None,
error_code_mapping: dict[str, str] | None = None,
intention: str | None = None,
**kwargs: object,
) -> dict[str, str]:
seen["label"] = self.current_label
return {"status": "ready"}
monkeypatch.setattr(script_skyvern_page.RealSkyvernPageAi, "ai_extract", ai_extract)
monkeypatch.setattr(app.AGENT_FUNCTION, "cleanup_element_tree_factory", lambda: None)
monkeypatch.setattr(app._inst, "scrape_exclude", None, raising=False)
recorder = CodeBlockActionRecording(
code_block=_make_code_block("data = await page.extract('Extract status')", goal="extract status"),
page=FakePageWithoutExtract(),
workflow_run_id="wr_test",
workflow_run_block_id="wrb_test",
organization_id="o_test",
workflow_run_context=FakeWorkflowRunContext(),
browser_state=FakeBrowserState(),
)
recorder.recording_page.current_label = "victim_block"
recorder.recording_page._RecordingPage__current_label = "victim_block" # type: ignore[attr-defined]
await recorder.recording_page.extract("Extract status")
assert seen["label"] == "code_1"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("ignore_workflow_system_prompt", "expected_prompt"),
[
(False, "Extract using only visible page facts."),
(True, None),
],
)
async def test_extract_bridge_materializes_workflow_system_prompt_decision(
monkeypatch: pytest.MonkeyPatch,
ignore_workflow_system_prompt: bool,
expected_prompt: str | None,
) -> None:
async def ai_extract(
self: AiExtractSelf,
prompt: str,
**kwargs: object,
) -> dict[str, str]:
return {"label": self.current_label or ""}
monkeypatch.setattr(script_skyvern_page.RealSkyvernPageAi, "ai_extract", ai_extract)
monkeypatch.setattr(app.AGENT_FUNCTION, "cleanup_element_tree_factory", lambda: None)
monkeypatch.setattr(app._inst, "scrape_exclude", None, raising=False)
context = FakeWorkflowRunContext()
context.workflow_system_prompt = "Extract using only visible page facts."
block = _make_code_block("data = await page.extract('Extract status')", goal="extract status")
block.ignore_workflow_system_prompt = ignore_workflow_system_prompt
recorder = CodeBlockActionRecording(
code_block=block,
page=FakePageWithoutExtract(),
workflow_run_id="wr_test",
workflow_run_block_id="wrb_test",
organization_id="o_test",
workflow_run_context=context,
browser_state=FakeBrowserState(),
)
result = await recorder.recording_page.extract("Extract status")
assert result == {"label": "code_1"}
assert context.get_block_workflow_system_prompt("code_1") == (True, expected_prompt)
@pytest.mark.asyncio
async def test_other_high_level_skyvern_page_calls_are_recorded() -> None:
"""High-level SkyvernPage methods without a prompt still record their action type."""
@ -372,6 +563,8 @@ class FakeWorkflowRunContext:
def __init__(self, secrets: dict[str, str] | None = None) -> None:
self.secrets = secrets or {}
self.block_workflow_system_prompts: dict[str, tuple[bool, str | None]] = {}
self.workflow_system_prompt: str | None = None
def get_block_metadata(self, label): # noqa: ANN001, ANN201
return {}
@ -379,6 +572,17 @@ class FakeWorkflowRunContext:
def build_workflow_run_summary(self) -> str:
return ""
def resolve_effective_workflow_system_prompt(self) -> str | None:
return self.workflow_system_prompt
def record_block_workflow_system_prompt(self, label: str, value: str | None) -> None:
self.block_workflow_system_prompts[label] = (True, value)
def get_block_workflow_system_prompt(self, label: str | None) -> tuple[bool, str | None]:
if label is None:
return False, None
return self.block_workflow_system_prompts.get(label, (False, None))
def get_value(self, key): # noqa: ANN001, ANN201
return self.values.get(key)

73
uv.lock generated
View file

@ -20,10 +20,9 @@ constraints = [
{ name = "flask", specifier = ">=3.1.3" },
{ name = "idna", specifier = ">=3.15" },
{ name = "joserfc", specifier = ">=1.6.8" },
{ name = "jupyter-server", specifier = ">=2.20.0" },
{ name = "jupyter-server", specifier = ">=2.18.0" },
{ name = "mako", specifier = ">=1.3.12" },
{ name = "mistune", specifier = ">=3.2.1" },
{ name = "msgpack", specifier = ">=1.2.1" },
{ name = "pyasn1", specifier = ">=0.6.3" },
{ name = "starlette", specifier = ">=1.3.1" },
{ name = "tornado", specifier = ">=6.5.6" },
@ -2500,7 +2499,7 @@ wheels = [
[[package]]
name = "jupyter-server"
version = "2.20.0"
version = "2.18.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -2523,9 +2522,9 @@ dependencies = [
{ name = "traitlets" },
{ name = "websocket-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/15/1eacb0fcb79ef86e8a0a79a708e6ad7435f6f223097dd29a4ce861fabc44/jupyter_server-2.18.2.tar.gz", hash = "sha256:06b4f40d8a7a00bb39d5216859c81374a0e7cfefe6d8a5a7facc5a5c37c679a7", size = 753177, upload-time = "2026-05-06T07:04:36.274Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" },
{ url = "https://files.pythonhosted.org/packages/e2/50/ecf4f70d65bdb7519b28a33d1b2fee8a4b4ba1ae1a92f15d97e877c5de21/jupyter_server-2.18.2-py3-none-any.whl", hash = "sha256:fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8", size = 391907, upload-time = "2026-05-06T07:04:34.014Z" },
]
[[package]]
@ -3094,43 +3093,37 @@ wheels = [
[[package]]
name = "msgpack"
version = "1.2.1"
version = "1.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" },
{ url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" },
{ url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" },
{ url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" },
{ url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" },
{ url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" },
{ url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" },
{ url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" },
{ url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" },
{ url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" },
{ url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" },
{ url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" },
{ url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" },
{ url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" },
{ url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" },
{ url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" },
{ url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" },
{ url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" },
{ url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" },
{ url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" },
{ url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" },
{ url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" },
{ url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" },
{ url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" },
{ url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" },
{ url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
{ url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
{ url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
{ url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
{ url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
{ url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
{ url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
{ url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
{ url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
{ url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
{ url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
{ url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
{ url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
{ url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
{ url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
{ url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
{ url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" },
{ url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" },
{ url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" },
{ url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" },
{ url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" },
{ url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" },
{ url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" },
{ url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" },
]
[[package]]