allow returning session bytes & metadata directly (#12)

This commit is contained in:
Vadym Barda
2025-05-16 10:40:22 -04:00
committed by GitHub
parent 7e3f3a5cb2
commit 3108f484f1
10 changed files with 676 additions and 534 deletions
+93 -33
View File
@@ -39,10 +39,9 @@ from langchain_sandbox import PyodideSandbox
# Create a sandbox instance
sandbox = PyodideSandbox(
"./sessions", # Directory to store session files
# Allow Pyodide to install python packages that
# might be required.
allow_net=True,
# Allow Pyodide to install python packages that
# might be required.
allow_net=True,
)
code = """\
import numpy as np
@@ -51,18 +50,47 @@ print(x)
"""
# Execute Python code
print(await sandbox.execute(code, session_id="123"))
print(await sandbox.execute(code))
# CodeExecutionResult(
# result=None,
# stdout='[1 2 3]',
# stderr=None,
# status='success',
# execution_time=2.8578367233276367
# execution_time=2.8578367233276367,
# session_metadata={'created': '2025-05-15T21:26:37.204Z', 'lastModified': '2025-05-15T21:26:37.831Z', 'packages': ['numpy']},
# session_bytes=None
# )
```
# Can still access a previous result!
print(await sandbox.execute("float(x[0])", session_id="123"))
### Stateful Sandbox
If you want to persist state between code executions (to persist variables, imports,
and definitions, etc.), you can set `stateful=True` in the sandbox. This will return
`session_bytes` and `session_metadata` that you can pass to `.execute()`.
> [!warning]
> `session_bytes` contains pickled session state. It should not be unpickled
> and is only meant to be used by the sandbox itself
```python
sandbox = PyodideSandbox(
# Create stateful sandbox
stateful=True,
# Allow Pyodide to install python packages that
# might be required.
allow_net=True,
)
code = """\
import numpy as np
x = np.array([1, 2, 3])
print(x)
"""
result = await sandbox.execute(code)
# Pass previous result
print(await sandbox.execute("float(x[0])", session_bytes=result.session_bytes, session_metadata=result.session_metadata))
# CodeExecutionResult(
# result=1,
@@ -70,6 +98,8 @@ print(await sandbox.execute("float(x[0])", session_id="123"))
# stderr=None,
# status='success',
# execution_time=2.7027177810668945
# session_metadata={'created': '2025-05-15T21:27:57.120Z', 'lastModified': '2025-05-15T21:28:00.061Z', 'packages': ['numpy', 'dill']},
# session_bytes=b'\x80\x04\x95d\x01\x00..."
# )
```
@@ -84,51 +114,81 @@ tool = PyodideSandboxTool()
result = await tool.ainvoke("print('Hello, world!')")
```
If you want to persist state between code executions (to persist variables, imports,
and definitions, etc.), you need to invoke the tool with `thread_id` in the config:
```python
code = """\
import numpy as np
x = np.array([1, 2, 3])
print(x)
"""
result = await tool.ainvoke(
code,
config={"configurable": {"thread_id": "123"}},
)
second_result = await tool.ainvoke(
"print(float(x[0]))", # tool is aware of the previous result
config={"configurable": {"thread_id": "123"}},
)
```
### Using with an agent
You can use `PyodideSandboxTool` inside a LangGraph agent. If you are using this tool inside an agent, you can invoke the agent with a config, and it will automatically be passed to the tool:
You can use sandbox tools inside a LangGraph agent:
```python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain_sandbox import PyodideSandboxTool
tool = PyodideSandboxTool()
tool = PyodideSandboxTool(
# Allow Pyodide to install python packages that
# might be required.
allow_net=True
)
agent = create_react_agent(
"anthropic:claude-3-7-sonnet-latest",
tools=[tool],
checkpointer=InMemorySaver()
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what's 5 + 7?"}]},
)
```
#### Stateful Tool
> [!important]
> **Stateful** `PyodideSandboxTool` works only in LangGraph agents that use the prebuilt [`create_react_agent`](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.chat_agent_executor.create_react_agent) or [`ToolNode`](https://langchain-ai.github.io/langgraph/reference/agents/#langgraph.prebuilt.tool_node.ToolNode).
If you want to persist state between code executions (to persist variables, imports,
and definitions, etc.), you need to set `stateful=True`:
```python
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
from langgraph.checkpoint.memory import InMemorySaver
from langchain_sandbox import PyodideSandboxTool, PyodideSandbox
class State(AgentState):
# important: add session_bytes & session_metadata keys to your graph state schema -
# these keys are required to store the session data between tool invocations.
# `session_bytes` contains pickled session state. It should not be unpickled
# and is only meant to be used by the sandbox itself
session_bytes: bytes
session_metadata: dict
tool = PyodideSandboxTool(
# Create stateful sandbox
stateful=True,
# Allow Pyodide to install python packages that
# might be required.
allow_net=True
)
agent = create_react_agent(
"anthropic:claude-3-7-sonnet-latest",
tools=[tool],
checkpointer=InMemorySaver(),
state_schema=State
)
result = await agent.ainvoke(
{
"messages": [
{"role": "user", "content": "what's 5 + 7? save result as 'a'"}
],
"session_bytes": None,
"session_metadata": None
},
config={"configurable": {"thread_id": "123"}},
)
second_result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what's the sine of that?"}]},
{"messages": [{"role": "user", "content": "what's the sine of 'a'?"}]},
config={"configurable": {"thread_id": "123"}},
)
```
See full examples here:
* [ReAct agent](examples/react_agent.py)
+6 -19
View File
@@ -6,24 +6,13 @@ from typing import Any
from langchain.chat_models import init_chat_model
from langchain_sandbox import PyodideSandbox
from langgraph.checkpoint.memory import MemorySaver
from langgraph_codeact import EvalCoroutine, create_codeact
def create_pyodide_eval_fn(
sandbox_dir: str = "./sessions", session_id: str | None = None
) -> EvalCoroutine:
def create_pyodide_eval_fn(sandbox: PyodideSandbox) -> EvalCoroutine:
"""Create an eval_fn that uses PyodideSandbox.
Args:
sandbox_dir: Directory to store session files
session_id: ID of the session to use
Returns:
A function that evaluates code using PyodideSandbox
"""
sandbox = PyodideSandbox(sandbox_dir, allow_net=True)
async def async_eval_fn(
code: str, _locals: dict[str, Any]
@@ -54,7 +43,6 @@ execute()
# Execute the code and get the result
response = await sandbox.execute(
code=context_setup + "\n\n" + wrapper_code,
session_id=session_id,
)
# Check if execution was successful
@@ -162,20 +150,19 @@ tools = [
model = init_chat_model("claude-3-7-sonnet-latest", model_provider="anthropic")
eval_fn = create_pyodide_eval_fn()
sandbox = PyodideSandbox(allow_net=True)
eval_fn = create_pyodide_eval_fn(sandbox)
code_act = create_codeact(model, tools, eval_fn)
agent = code_act.compile(checkpointer=MemorySaver())
agent = code_act.compile()
query = """A batter hits a baseball at 45.847 m/s at an angle of 23.474° above the horizontal. The outfielder, who starts facing the batter, picks up the baseball as it lands, then throws it back towards the batter at 24.12 m/s at an angle of 39.12 degrees. How far is the baseball from where the batter originally hit it? Assume zero air resistance."""
async def run_agent(query: str, thread_id: str):
config = {"configurable": {"thread_id": thread_id}}
async def run_agent(query: str):
# Stream agent outputs
async for typ, chunk in agent.astream(
{"messages": query},
stream_mode=["values", "messages"],
config=config,
):
if typ == "messages":
print(chunk[0].content, end="")
@@ -185,4 +172,4 @@ async def run_agent(query: str, thread_id: str):
if __name__ == "__main__":
# Run the agent
asyncio.run(run_agent(query, str(uuid.uuid4())))
asyncio.run(run_agent(query))
+7 -15
View File
@@ -1,39 +1,31 @@
# pip install langgraph "langchain[anthropic]"
import asyncio
import uuid
from langchain_sandbox import PyodideSandbox, PyodideSandboxTool
from langchain_sandbox import PyodideSandboxTool
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import InMemorySaver
# Create a sandbox instance
sandbox = PyodideSandbox(
"./sessions", # Directory to store session files
# Define the sandbox tool
sandbox_tool = PyodideSandboxTool(
# Allow Pyodide to install python packages that
# might be required.
allow_net=True,
)
# Define the sandbox tool
sandbox_tool = PyodideSandboxTool(sandbox=sandbox)
checkpointer = InMemorySaver()
# Create an agent with the sandbox tool
agent = create_react_agent(
"anthropic:claude-3-7-sonnet-latest", [sandbox_tool], checkpointer=checkpointer
"anthropic:claude-3-7-sonnet-latest", [sandbox_tool]
)
query = """A batter hits a baseball at 45.847 m/s at an angle of 23.474° above the horizontal. The outfielder, who starts facing the batter, picks up the baseball as it lands, then throws it back towards the batter at 24.12 m/s at an angle of 39.12 degrees. How far is the baseball from where the batter originally hit it? Assume zero air resistance."""
async def run_agent(query: str, thread_id: str):
config = {"configurable": {"thread_id": thread_id}}
async def run_agent(query: str):
# Stream agent outputs
async for chunk in agent.astream({"messages": query}, config):
async for chunk in agent.astream({"messages": query}):
print(chunk)
print("\n")
if __name__ == "__main__":
# Run the agent
asyncio.run(run_agent(query, str(uuid.uuid4())))
asyncio.run(run_agent(query))
+70 -151
View File
@@ -88,18 +88,23 @@ async def install_imports(
return to_install
def load_session(path: str) -> List[str]:
def load_session_bytes(session_bytes: bytes) -> list[str]:
"""Load the session module."""
import dill
import io
dill.session.load_session(filename=path)
buffer = io.BytesIO(session_bytes.to_py())
dill.session.load_session(filename=buffer)
def dump_session(path: str) -> None:
def dump_session_bytes() -> bytes:
"""Dump the session module."""
import dill
import io
dill.session.dump_session(filename=path)
buffer = io.BytesIO()
dill.session.dump_session(filename=buffer)
return buffer.getvalue()
def robust_serialize(obj):
@@ -159,6 +164,8 @@ interface PyodideResult {
stderr?: string[];
error?: string;
jsonResult?: string;
sessionBytes?: Uint8Array;
sessionMetadata?: SessionMetadata;
}
async function initPyodide(pyodide: any): Promise<void> {
@@ -174,8 +181,9 @@ async function initPyodide(pyodide: any): Promise<void> {
async function runPython(
pythonCode: string,
options: {
session?: string;
sessionsDir?: string;
stateful?: boolean;
sessionBytes?: string;
sessionMetadata?: string;
}
): Promise<PyodideResult> {
const output: string[] = [];
@@ -197,95 +205,40 @@ async function runPython(
await initPyodide(pyodide);
// Determine session directory
const sessionsDir = options.sessionsDir || Deno.cwd();
let sessionMetadata: SessionMetadata = {
created: new Date().toISOString(),
lastModified: new Date().toISOString(),
packages: [],
let sessionMetadata: SessionMetadata;
if (options.sessionMetadata) {
sessionMetadata = JSON.parse(options.sessionMetadata);
} else {
sessionMetadata = {
created: new Date().toISOString(),
lastModified: new Date().toISOString(),
packages: [],
};
};
let sessionJsonPath: string;
let isExistingSession = false;
let sessionData: Uint8Array | null = null;
// Handle session if provided
if (options.session) {
// Create session directory path
const sessionDirPath = join(sessionsDir, options.session);
const sessionPklPath = join(sessionDirPath, `session.pkl`);
sessionJsonPath = join(sessionDirPath, `session.json`);
// Ensure session directory exists
try {
const dirInfo = await Deno.stat(sessionDirPath);
if (!dirInfo.isDirectory) {
console.error(`Path exists but is not a directory: ${sessionDirPath}`);
return { success: false, error: `Path exists but is not a directory: ${sessionDirPath}` };
}
} catch (error) {
// Directory doesn't exist, create it
if (error instanceof Deno.errors.NotFound) {
try {
await Deno.mkdir(sessionDirPath, { recursive: true });
} catch (mkdirError: any) {
console.error(`Error creating session directory: ${mkdirError.message}`);
return { success: false, error: mkdirError.message };
}
} else {
console.error(`Error accessing session directory: ${(error as Error).message}`);
return { success: false, error: (error as Error).message };
}
}
try {
// Check if both session pickle and metadata files exist
const [pklStat, jsonStat] = await Promise.all([
Deno.stat(sessionPklPath),
Deno.stat(sessionJsonPath)
]).catch(() => [null, null]);
isExistingSession = (pklStat?.isFile && jsonStat?.isFile) || false;
} catch {
// Error checking files, assume they don't exist
isExistingSession = false;
}
// Create or load session metadata
if (!isExistingSession) {
// Create new session metadata file
await Deno.writeTextFile(
sessionJsonPath,
JSON.stringify(sessionMetadata, null, 2)
);
} else {
// Load existing session metadata
const jsonContent = await Deno.readTextFile(sessionJsonPath);
sessionMetadata = JSON.parse(jsonContent);
}
// Load PKL file into pyodide if it exists
try {
const sessionData = await Deno.readFile(sessionPklPath);
pyodide.FS.writeFile(`/${options.session}.pkl`, sessionData);
} catch (error) {
// File doesn't exist or can't be read, skip loading
}
if (options.sessionBytes && !options.sessionMetadata) {
console.error("sessionMetadata is required when providing sessionBytes");
return { success: false, error: "sessionMetadata is required when providing sessionBytes" };
}
// Import our prepared environment module
const prepare_env = pyodide.pyimport("prepare_env");
// Prepare additional packages to install (include dill if using sessions)
const additionalPackagesToInstall = options.session
? [...new Set([...sessionMetadata.packages, "dill"])]
: [];
// Prepare additional packages to install (include dill)
const defaultPackages = options.stateful ? ["dill"] : [];
const additionalPackagesToInstall = options.sessionBytes
? [...new Set([...defaultPackages, ...sessionMetadata.packages])]
: defaultPackages;
const installedPackages = await prepare_env.install_imports(
pythonCode,
additionalPackagesToInstall,
);
if (options.session && isExistingSession) {
if (options.sessionBytes) {
sessionData = Uint8Array.from(JSON.parse(options.sessionBytes));
// Run session preamble
await prepare_env.load_session(`/${options.session}.pkl`);
await prepare_env.load_session_bytes(sessionData);
}
const packages = installedPackages.map((pkg: any) => pkg.get("package"));
@@ -296,35 +249,30 @@ async function runPython(
const rawValue = await pyodide.runPythonAsync(pythonCode);
// Dump result to string
const jsonValue = await prepare_env.dumps(rawValue);
if (options.session) {
// Save session state
await prepare_env.dump_session(`/${options.session}.pkl`);
// Update session metadata with installed packages
sessionMetadata.packages = [
...new Set([...sessionMetadata.packages, ...packages]),
];
sessionMetadata.lastModified = new Date().toISOString();
await Deno.writeTextFile(
sessionJsonPath as string,
JSON.stringify(sessionMetadata, null, 2)
);
// Update session metadata with installed packages
sessionMetadata.packages = [
...new Set([...sessionMetadata.packages, ...packages]),
];
sessionMetadata.lastModified = new Date().toISOString();
// Save session file back to host machine
const sessionData = pyodide.FS.readFile(`/${options.session}.pkl`);
const sessionDirPath = join(sessionsDir, options.session);
const sessionPklPath = join(sessionDirPath, `session.pkl`);
await Deno.writeFile(sessionPklPath, sessionData);
}
if (options.stateful) {
// Save session state to sessionBytes
sessionData = await prepare_env.dump_session_bytes() as Uint8Array;
};
// Return the result with stdout and stderr output
return {
const result: PyodideResult = {
success: true,
result: rawValue,
jsonResult: jsonValue,
stdout: output,
stderr: err_output
stderr: err_output,
sessionMetadata: sessionMetadata,
};
if (options.stateful && sessionData) {
result["sessionBytes"] = sessionData;
}
return result;
} catch (error: any) {
return {
success: false,
@@ -337,17 +285,18 @@ async function runPython(
async function main(): Promise<void> {
const flags = parseArgs(Deno.args, {
string: ["code", "file", "session", "sessions-dir"],
string: ["code", "file", "session-bytes", "session-metadata"],
alias: {
c: "code",
f: "file",
s: "session",
d: "sessions-dir",
h: "help",
V: "version",
s: "stateful",
b: "session-bytes",
m: "session-metadata",
},
boolean: ["help", "version"],
default: { help: false, version: false },
boolean: ["help", "version", "stateful"],
default: { help: false, version: false, stateful: false },
});
if (flags.help) {
@@ -358,8 +307,9 @@ Run Python code in a sandboxed environment using Pyodide
OPTIONS:
-c, --code <code> Python code to execute
-f, --file <path> Path to Python file to execute
-s, --session <string> Session name
-d, --sessions-dir <path> Directory to store session files
-s, --stateful <bool> Use a stateful session
-b, --session-bytes <bytes> Session bytes
-m, --session-metadata Session metadata
-h, --help Display help
-V, --version Display version
`);
@@ -368,14 +318,15 @@ OPTIONS:
if (flags.version) {
console.log(pkgVersion)
return;
return
}
const options = {
code: flags.code,
file: flags.file,
session: flags.session,
sessionsDir: flags["sessions-dir"],
stateful: flags.stateful,
sessionBytes: flags["session-bytes"],
sessionMetadata: flags["session-metadata"],
};
if (!options.code && !options.file) {
@@ -385,41 +336,6 @@ OPTIONS:
Deno.exit(1);
}
// Validate session ID if provided
if (options.session) {
const validSessionIdRegex = /^[a-zA-Z0-9_-]+$/;
if (!validSessionIdRegex.test(options.session)) {
console.error(
"Error: Session ID must only contain letters, numbers, underscores, and hyphens."
);
Deno.exit(1);
}
}
// Ensure sessions directory exists if specified
if (options.sessionsDir) {
try {
try {
const dirInfo = await Deno.stat(options.sessionsDir);
if (!dirInfo.isDirectory) {
throw new Error(
`Path exists but is not a directory: ${options.sessionsDir}`
);
}
} catch (error) {
// Directory doesn't exist, create it
if (error instanceof Deno.errors.NotFound) {
await Deno.mkdir(options.sessionsDir, { recursive: true });
} else {
throw error;
}
}
} catch (error: any) {
console.error(`Error creating sessions directory: ${error.message}`);
Deno.exit(1);
}
}
// Get Python code from file or command line argument
let pythonCode = "";
@@ -440,8 +356,9 @@ OPTIONS:
}
const result = await runPython(pythonCode, {
session: options.session,
sessionsDir: options.sessionsDir,
stateful: options.stateful,
sessionBytes: options.sessionBytes,
sessionMetadata: options.sessionMetadata,
});
// Exit with error code if Python execution failed
@@ -451,6 +368,8 @@ OPTIONS:
stderr: result.success ? (result.stderr.join('') || null) : result.error || null,
result: result.success ? JSON.parse(result.jsonResult || 'null') : null,
success: result.success,
sessionBytes: result.sessionBytes,
sessionMetadata: result.sessionMetadata,
};
// Output as JSON to stdout
@@ -1,7 +1,9 @@
"""LangChain code sandbox."""
from langchain_sandbox.pyodide import PyodideSandbox
from langchain_sandbox.tool import PyodideSandboxTool
from langchain_sandbox.pyodide import (
PyodideSandbox,
PyodideSandboxTool,
)
__all__ = [
"PyodideSandbox",
+252 -63
View File
@@ -4,10 +4,18 @@ import asyncio
import dataclasses
import json
import logging
import re
import subprocess
import time
from typing import Any, Literal
from typing import Annotated, Any, Literal
from langchain_core.callbacks import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
)
from langchain_core.messages import ToolMessage
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import BaseTool, InjectedToolCallId
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
@@ -24,8 +32,11 @@ class CodeExecutionResult:
stderr: str | None = None
status: Status
execution_time: float
session_metadata: dict | None = None
session_bytes: bytes | None = None
# Published package name
PKG_NAME = "jsr:@eyurtsev/test-sandbox@0.0.7"
@@ -79,10 +90,10 @@ class PyodideSandbox:
- Streaming stdout/stderr capture
"""
def __init__( # noqa: PLR0913
def __init__(
self,
sessions_dir: str,
*,
stateful: bool = False,
allow_env: list[str] | bool = False,
allow_read: list[str] | bool = False,
allow_write: list[str] | bool = False,
@@ -99,9 +110,11 @@ class PyodideSandbox:
based on the needs of the code being executed.
Args:
sessions_dir: Directory for storing session data. This directory must
be writable by the Deno subprocess. It is used to persist session
state between executions.
stateful: Whether to use a stateful session. If True, `sandbox.execute`
will include session metadata and the session bytes containing the
session state (variables, imports, etc.) in the execution result.
This allows saving and reusing the session state between executions.
allow_env: Environment variable access configuration:
- False: No environment access (default, most secure)
- True: Unrestricted access to all environment variables
@@ -114,7 +127,7 @@ class PyodideSandbox:
- List[str]: Read access restricted to specific paths, e.g.
["/tmp/sandbox", "./data"]
By default allows read to node_modules and to sessions dir
By default allows read from node_modules
allow_write: File system write access configuration:
- False: No file system write access (default, most secure)
@@ -122,7 +135,7 @@ class PyodideSandbox:
- List[str]: Write access restricted to specific paths, e.g.
["/tmp/sandbox/output"]
By default allows read to node_modules and to sessions dir
By default allows write to node_modules
allow_net: Network access configuration:
- False: No network access (default, most secure)
@@ -146,15 +159,7 @@ class PyodideSandbox:
the default directory for Deno modules.
"""
if "," in sessions_dir:
# Very simple check to protect a user against typos.
# The goal isn't to be exhaustive on validation here.
msg = "Please provide a valid session directory."
raise ValueError(msg)
# Store configuration
self.sessions_dir = sessions_dir
self.stateful = stateful
# Configure permissions
self.permissions = []
@@ -173,9 +178,9 @@ class PyodideSandbox:
perm_defs = [
("--allow-env", allow_env, None),
# For file system permissions, if no permission is specified,
# force session_dir and node_modules
("--allow-read", allow_read, [sessions_dir, "node_modules"]),
("--allow-write", allow_write, [sessions_dir, "node_modules"]),
# force node_modules
("--allow-read", allow_read, ["node_modules"]),
("--allow-write", allow_write, ["node_modules"]),
("--allow-net", allow_net, None),
("--allow-run", allow_run, None),
("--allow-ffi", allow_ffi, None),
@@ -192,42 +197,12 @@ class PyodideSandbox:
self.permissions.append(f"--node-modules-dir={node_modules_dir}")
# Regular expression for validating session IDs
self.session_id_pattern = re.compile(r"^[a-zA-Z0-9\-_]+$")
def _validate_session_id(self, session_id: str | None) -> str | None:
"""Validate the session ID against the allowed pattern.
Args:
session_id: The session ID to validate
Returns:
The session ID if valid, None otherwise
Raises:
ValueError: If the session ID contains invalid characters
"""
if session_id is None:
return None
if not self.session_id_pattern.match(session_id):
msg = (
f"Invalid session ID: {session_id}. "
"Session IDs must contain only alphanumeric characters, "
"hyphens, and underscores."
)
raise ValueError(
msg,
)
return session_id
async def execute(
self,
code: str,
*,
session_id: str | None = None,
session_bytes: bytes | None = None,
session_metadata: dict | None = None,
timeout_seconds: float | None = None,
memory_limit_mb: int | None = None,
) -> CodeExecutionResult:
@@ -253,10 +228,13 @@ class PyodideSandbox:
Args:
code: The Python code to execute in the sandbox
session_id: Optional session identifier for maintaining state between
executions. Can be used to persist variables, imports,
and definitions across multiple execute() calls. If None,
a new session is created.
session_bytes: Optional bytes to be used as the initial session state.
This is used to resume code execution from the same session.
You can use this instead of `session_id`.
Note: when using this, you need to provide `session_metadata`.
session_metadata: Optional metadata to be used as the initial session state.
This is used to resume code execution from the same session.
Note: when using this, you need to provide `session_bytes`.
timeout_seconds: Maximum execution time in seconds before the process
is terminated. If None, execution may run indefinitely
(not recommended for untrusted code).
@@ -298,18 +276,21 @@ class PyodideSandbox:
cmd.append(f"--v8-flags=--max-old-space-size={memory_limit_mb}")
# Add the path to the JavaScript wrapper script
# Developer version
cmd.append(PKG_NAME)
# Add script path and code
cmd.extend(["-c", code])
# Add session ID if provided
if session_id:
cmd.extend(["-s", session_id])
if self.stateful:
cmd.extend(["-s"])
# Ensure the sessions directory exists
cmd.extend(["-d", self.sessions_dir])
if session_bytes:
# Convert bytes to list of integers and then to JSON string
bytes_array = list(session_bytes)
cmd.extend(["-b", json.dumps(bytes_array)])
if session_metadata:
cmd.extend(["-m", json.dumps(session_metadata)])
# Create and run the subprocess
process = await asyncio.create_subprocess_exec(
@@ -334,6 +315,12 @@ class PyodideSandbox:
stderr = full_result.get("stderr", None)
result = full_result.get("result", None)
status = "success" if full_result.get("success", False) else "error"
session_metadata = full_result.get("sessionMetadata", None)
# Convert the Uint8Array to Python bytes
session_bytes_array = full_result.get("sessionBytes", None)
session_bytes = (
bytes(session_bytes_array) if session_bytes_array else None
)
else:
stderr = stderr_bytes.decode("utf-8", errors="replace")
status = "error"
@@ -353,4 +340,206 @@ class PyodideSandbox:
stdout=stdout or None,
stderr=stderr or None,
result=result,
session_metadata=session_metadata,
session_bytes=session_bytes,
)
class PyodideSandboxTool(BaseTool):
"""Tool for running python code in a PyodideSandbox.
If you use a stateful sandbox (PyodideSandboxTool(stateful=True)),
the state between code executions (to variables, imports,
and definitions, etc.), will be persisted using LangGraph checkpointer.
!!! important
When you use a stateful sandbox, this tool can only be used
inside a LangGraph graph with a checkpointer, and
has to be used with the prebuilt `create_react_agent` or `ToolNode`.
Example: stateless sandbox usage
```python
from langgraph.prebuilt import create_react_agent
from langchain_sandbox import PyodideSandboxTool
tool = PyodideSandboxTool(allow_net=True)
agent = create_react_agent(
"anthropic:claude-3-7-sonnet-latest",
tools=[tool],
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what's 5 + 7?"}]},
)
```
Example: stateful sandbox usage
```python
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
from langgraph.checkpoint.memory import InMemorySaver
from langchain_sandbox import PyodideSandboxTool, PyodideSandbox
class State(AgentState):
session_bytes: bytes
session_metadata: dict
tool = PyodideSandboxTool(stateful=True, allow_net=True)
agent = create_react_agent(
"anthropic:claude-3-7-sonnet-latest",
tools=[tool],
checkpointer=InMemorySaver(),
state_schema=State
)
result = await agent.ainvoke(
{
"messages": [
{"role": "user", "content": "what's 5 + 7? save result as 'a'"}
],
"session_bytes": None,
"session_metadata": None
},
config={"configurable": {"thread_id": "123"}},
)
second_result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what's the sine of 'a'?"}]},
config={"configurable": {"thread_id": "123"}},
)
```
"""
name: str = "python_code_sandbox"
description: str = (
"A secure Python code sandbox. Use this to execute python commands.\n"
"- Input should be a valid python command.\n"
"- To return output, you should print it out with `print(...)`.\n"
"- Don't use f-strings when printing outputs.\n"
"- If you need to make web requests, use `httpx.AsyncClient`."
)
# Mirror the PyodideSandbox constructor arguments
stateful: bool = False
allow_env: list[str] | bool = False
allow_read: list[str] | bool = False
allow_write: list[str] | bool = False
allow_net: list[str] | bool = False
allow_run: list[str] | bool = False
allow_ffi: list[str] | bool = False
node_modules_dir: str = "auto"
_sandbox: PyodideSandbox
def __init__(self, **kwargs: dict[str, Any]) -> None:
"""Initialize the tool."""
super().__init__(**kwargs)
if self.stateful:
try:
from langgraph.prebuilt import InjectedState
except ImportError as e:
error_msg = (
"The 'langgraph' package is required when using a stateful sandbox."
" Please install it with 'pip install langgraph'."
)
raise ImportError(error_msg) from e
class PyodideSandboxToolInput(BaseModel):
"""Python code to execute in the sandbox."""
code: str = Field(description="Code to execute.")
# these fields will be ignored by the LLM
# and automatically injected by LangGraph's ToolNode
state: Annotated[dict[str, Any] | BaseModel, InjectedState]
tool_call_id: Annotated[str, InjectedToolCallId]
else:
class PyodideSandboxToolInput(BaseModel):
"""Python code to execute in the sandbox."""
code: str = Field(description="Code to execute.")
self.args_schema: type[BaseModel] = PyodideSandboxToolInput
self._sandbox = PyodideSandbox(
stateful=self.stateful,
allow_env=self.allow_env,
allow_read=self.allow_read,
allow_write=self.allow_write,
allow_net=self.allow_net,
allow_run=self.allow_run,
allow_ffi=self.allow_ffi,
node_modules_dir=self.node_modules_dir,
)
def _run(
self,
code: str,
state: dict[str, Any] | BaseModel | None = None,
tool_call_id: str | None = None,
config: RunnableConfig | None = None,
run_manager: CallbackManagerForToolRun | None = None,
) -> Any: # noqa: ANN401
"""Use the tool."""
error_msg = (
"Sync invocation of PyodideSandboxTool is not supported - "
"please invoke the tool asynchronously using `await tool.ainvoke()`"
)
raise NotImplementedError(error_msg)
async def _arun(
self,
code: str,
state: dict[str, Any] | BaseModel | None = None,
tool_call_id: str | None = None,
config: RunnableConfig | None = None,
run_manager: AsyncCallbackManagerForToolRun | None = None,
) -> Any: # noqa: ANN401
"""Use the tool asynchronously."""
if self.stateful:
required_keys = {"session_bytes", "session_metadata", "messages"}
actual_keys = set(state) if isinstance(state, dict) else set(state.__dict__)
if missing_keys := required_keys - actual_keys:
error_msg = (
"Input state is missing "
f"the following required keys: {missing_keys}"
)
raise ValueError(error_msg)
if isinstance(state, dict):
session_bytes = state["session_bytes"]
session_metadata = state["session_metadata"]
else:
session_bytes = state.session_bytes
session_metadata = state.session_metadata
result = await self._sandbox.execute(
code, session_bytes=session_bytes, session_metadata=session_metadata
)
else:
result = await self._sandbox.execute(code)
if result.stderr:
tool_result = f"Error during execution: {result.stderr}"
else:
tool_result = result.stdout
if self.stateful:
from langgraph.types import Command
# if the tool is used with a stateful sandbox,
# we need to update the graph state with the new session bytes and metadata
return Command(
update={
"session_bytes": result.session_bytes,
"session_metadata": result.session_metadata,
"messages": [
ToolMessage(
content=tool_result,
tool_call_id=tool_call_id,
)
],
}
)
return tool_result
-106
View File
@@ -1,106 +0,0 @@
"""LangChain tool for running python code in a PyodideSandbox."""
from langchain_core.callbacks import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
)
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
from langchain_sandbox import PyodideSandbox
def _get_default_pyodide_sandbox() -> PyodideSandbox:
"""Get default sandbox for the tool."""
return PyodideSandbox(
"./sessions", # Directory to store session files
# Allow Pyodide to install python packages that
# might be required.
allow_net=True,
)
class PythonInputs(BaseModel):
"""Python code to execute in the sandbox."""
code: str = Field(description="Code to execute.")
class PyodideSandboxTool(BaseTool):
"""Tool for running python code in a PyodideSandbox.
If you want to persist state between code executions (to persist variables, imports,
and definitions, etc.), you need to invoke the tool with `thread_id` in the config:
```python
from langchain_sandbox import PyodideSandboxTool
tool = PyodideSandboxTool()
result = await tool.ainvoke(
"print('Hello, world!')",
config={"configurable": {"thread_id": "123"}},
)
```
If you are using this tool inside an agent, like LangGraph `create_react_agent`, you
can invoke the agent with a config, and it will automatically be passed to the tool:
```python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain_sandbox import PyodideSandboxTool
tool = PyodideSandboxTool()
agent = create_react_agent(
"anthropic:claude-3-7-sonnet-latest",
tools=[tool],
checkpointer=InMemorySaver()
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what's 5 + 7?"}]},
config={"configurable": {"thread_id": "123"}},
)
second_result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "what's the sine of that?"}]},
config={"configurable": {"thread_id": "123"}},
)
```
"""
name: str = "python_code_sandbox"
description: str = (
"A secure Python code sandbox. Use this to execute python commands.\n"
"- Input should be a valid python command.\n"
"- To return output, you should print it out with `print(...)`.\n"
"- Don't use f-strings when printing outputs.\n"
"- If you need to make web requests, use `httpx.AsyncClient`."
)
sandbox: PyodideSandbox = Field(default_factory=_get_default_pyodide_sandbox)
args_schema: type[BaseModel] = PythonInputs
def _run(
self,
code: str,
config: RunnableConfig,
run_manager: CallbackManagerForToolRun | None = None,
) -> str:
"""Use the tool."""
error_msg = (
"Sync invocation of PyodideSandboxTool is not supported - "
"please invoke the tool asynchronously using `await tool.ainvoke()`"
)
raise NotImplementedError(error_msg)
async def _arun(
self,
code: str,
config: RunnableConfig,
run_manager: AsyncCallbackManagerForToolRun | None = None,
) -> str:
"""Use the tool asynchronously."""
session_id = config.get("configurable", {}).get("thread_id")
result = await self.sandbox.execute(code, session_id=session_id)
if result.stderr:
return f"Error during execution: {result.stderr}"
return result.stdout
+3 -1
View File
@@ -5,7 +5,8 @@ description = "LangChain Sandbox"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-core>=0.3.56,<0.4.0"
"langchain-core>=0.3.56,<0.4.0",
"langgraph>=0.4.3"
]
[dependency-groups]
@@ -46,6 +47,7 @@ select = [
ignore = [
"ARG",
"COM812",
"PLR0913",
]
@@ -1,166 +1,90 @@
"""Test pyodide sandbox functionality."""
import shutil
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
import pytest
from langchain_sandbox import PyodideSandbox
@contextmanager
def sandbox_context( # noqa: PLR0913
*,
allow_read: list[str] | str | bool = "node_modules",
allow_write: list[str] | str | bool = "node_modules",
allow_net: list[str] | bool = True,
allow_env: list[str] | bool = False,
allow_run: list[str] | bool = False,
allow_ffi: list[str] | bool = False,
) -> Iterator[tuple[PyodideSandbox, str]]:
"""Create a PyodideSandbox instance with a temporary directory for sessions.
This context manager creates a sandbox with a temporary directory and ensures
cleanup when the context exits.
Args:
allow_read: File system read permissions
allow_write: File system write permissions
allow_net: Network access permissions
allow_env: Environment variable access permissions
allow_run: Subprocess execution permissions
allow_ffi: Foreign Function Interface permissions
Yields:
A tuple containing (sandbox_instance, temp_directory_path)
"""
# Create temporary directory
temp_sessions_dir = tempfile.mkdtemp(prefix="pyodide_test_sessions_")
try:
# Set default permissions for temp dir
actual_read = [] if allow_read is None else allow_read
actual_write = [] if allow_write is None else allow_write
# convert str to list
if isinstance(actual_read, str):
actual_read = [actual_read]
if isinstance(actual_write, str):
actual_write = [actual_write]
# Ensure the temp directory is always readable and writable
if isinstance(actual_read, list) and temp_sessions_dir not in actual_read:
actual_read.append(temp_sessions_dir)
if isinstance(actual_write, list) and temp_sessions_dir not in actual_write:
actual_write.append(temp_sessions_dir)
# Create the sandbox
sandbox = PyodideSandbox(
sessions_dir=temp_sessions_dir,
allow_read=actual_read,
allow_write=actual_write,
allow_net=allow_net,
allow_env=allow_env,
allow_run=allow_run,
allow_ffi=allow_ffi,
)
# Yield the sandbox and temp dir
yield sandbox, temp_sessions_dir
finally:
# Clean up the temporary directory
shutil.rmtree(temp_sessions_dir, ignore_errors=True)
current_dir = Path(__file__).parent
async def test_stdout_sessionless() -> None:
@pytest.fixture
def pyodide_package(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch PKG_NAME to point to a local deno typescript file."""
local_script = str(current_dir / "../../../pyodide-sandbox-js/main.ts")
monkeypatch.setattr("langchain_sandbox.pyodide.PKG_NAME", local_script)
def get_default_sandbox(stateful: bool = False) -> PyodideSandbox:
"""Get default PyodideSandbox instance for testing."""
return PyodideSandbox(
stateful=stateful,
allow_read=True,
allow_write=True,
allow_net=True,
allow_env=False,
allow_run=False,
allow_ffi=False,
)
async def test_stdout_sessionless(pyodide_package: None) -> None:
"""Test without a session ID."""
with sandbox_context() as (sandbox, _):
# Execute a simple piece of code synchronously
result = await sandbox.execute("x = 5; print(x); x")
assert result.status == "success"
assert result.stdout == "5"
assert result.result == 5
assert result.stderr is None
sandbox = get_default_sandbox()
# Execute a simple piece of code synchronously
result = await sandbox.execute("x = 5; print(x); x")
assert result.status == "success"
assert result.stdout == "5"
assert result.result == 5
assert result.stderr is None
assert result.session_bytes is None
async def test_session_state_persistence_basic() -> None:
async def test_session_state_persistence_basic(pyodide_package: None) -> None:
"""Simple test to verify that a session ID is used to persist state.
We'll assign a variable in one execution and check if it's available in the next.
"""
with sandbox_context() as (sandbox, _):
# Test with a session ID to ensure state persistence
session_id = "test_session_1"
result1 = await sandbox.execute("y = 10; print(y)", session_id=session_id)
result2 = await sandbox.execute("print(y)", session_id=session_id)
sandbox = get_default_sandbox(stateful=True)
# Check session state persistence
assert result1.status == "success", f"Encountered error: {result1.stderr}"
assert result1.stdout == "10"
assert result1.result is None
assert result2.status == "success", f"Encountered error: {result2.stderr}"
assert result2.stdout == "10"
assert result1.result is None
result1 = await sandbox.execute("y = 10; print(y)")
result2 = await sandbox.execute(
"print(y)",
session_bytes=result1.session_bytes,
session_metadata=result1.session_metadata,
)
# Check session state persistence
assert result1.status == "success", f"Encountered error: {result1.stderr}"
assert result1.stdout == "10"
assert result1.result is None
assert result2.status == "success", f"Encountered error: {result2.stderr}"
assert result2.stdout == "10"
assert result1.result is None
async def test_pyodide_sandbox_error_handling() -> None:
async def test_pyodide_sandbox_error_handling(pyodide_package: None) -> None:
"""Test PyodideSandbox error handling."""
with sandbox_context() as (sandbox, _):
# Test syntax error
result = await sandbox.execute("x = 5; y = x +")
assert result.status == "error"
assert "SyntaxError" in result.stderr
sandbox = get_default_sandbox()
# Test undefined variable error
result = await sandbox.execute("undefined_variable")
assert result.status == "error"
assert "NameError" in result.stderr
# Test syntax error
result = await sandbox.execute("x = 5; y = x +")
assert result.status == "error"
assert "SyntaxError" in result.stderr
# Test undefined variable error
result = await sandbox.execute("undefined_variable")
assert result.status == "error"
assert "NameError" in result.stderr
async def test_pyodide_sandbox_timeout() -> None:
async def test_pyodide_sandbox_timeout(pyodide_package: None) -> None:
"""Test PyodideSandbox timeout handling."""
with sandbox_context() as (sandbox, _):
# Test timeout with infinite loop
# Using a short timeout to avoid long test runs
result = await sandbox.execute("while True: pass", timeout_seconds=0.5)
assert result.status == "error"
assert "timed out" in result.stderr.lower()
sandbox = get_default_sandbox()
# Currently, we do not support file operations persisting across sessions
async def test_pyodide_sandbox_file_operations() -> None:
"""Test file operations are not persisted across sessions."""
with sandbox_context() as (sandbox, _):
# Test file I/O within the allowed session directory
# Note: In Pyodide, the sessions directory might be mapped differently
# We are testing the ability to write files via the sandbox,
# which is what matters
session_id = "file_test_session"
# Create a file in the session
code_write = """
import json
data = {'test': 'value', 'number': 42}
with open('test_data.json', 'w') as f:
json.dump(data, f)
"""
result = await sandbox.execute(code_write, session_id=session_id)
assert result.status == "success"
assert result.stdout is None
assert result.stderr is None
assert result.result is None
# Confirm that the file does not exist in the session directory
code_read = """\
import os
if os.path.exists('test_data.json'):
raise Exception("File should not exist in the session directory.")
print("OK")
"""
result = await sandbox.execute(code_read, session_id=session_id)
assert result.status == "error"
assert result.stdout is None
assert "File should not exist" in result.stderr
# Test timeout with infinite loop
# Using a short timeout to avoid long test runs
result = await sandbox.execute("while True: pass", timeout_seconds=0.5)
assert result.status == "error"
assert "timed out" in result.stderr.lower()
+174 -1
View File
@@ -340,6 +340,7 @@ version = "0.0.4"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph" },
]
[package.dev-dependencies]
@@ -354,7 +355,10 @@ test = [
]
[package.metadata]
requires-dist = [{ name = "langchain-core", specifier = ">=0.3.56,<0.4.0" }]
requires-dist = [
{ name = "langchain-core", specifier = ">=0.3.56,<0.4.0" },
{ name = "langgraph", specifier = ">=0.4.3" },
]
[package.metadata.requires-dev]
test = [
@@ -367,6 +371,62 @@ test = [
{ name = "ruff", specifier = ">=0.9.7" },
]
[[package]]
name = "langgraph"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core", marker = "python_full_version < '4.0'" },
{ name = "langgraph-checkpoint" },
{ name = "langgraph-prebuilt", marker = "python_full_version < '4.0'" },
{ name = "langgraph-sdk", marker = "python_full_version < '4.0'" },
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/9e/5a64602eff18a99d0216a80eff823051ffbdb7c11b5a16171cee8b1ccce5/langgraph-0.4.3.tar.gz", hash = "sha256:272d5d5903f2c2882dbeeba849846a0f2500bd83fb3734a3801ebe64c1a60bdd", size = 125407 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/53/0a20edd9f41eb3707722444ec1b43752b792bbe904d1c8cc3ba27f8eb2c8/langgraph-0.4.3-py3-none-any.whl", hash = "sha256:dec926e034f4d440b92a3c52139cb6e9763bc1791e79a6ea53a233309cec864f", size = 151191 },
]
[[package]]
name = "langgraph-checkpoint"
version = "2.0.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "ormsgpack" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/72/d49828e6929cb3ded1472aa3e5e4a369d292c4f21021ac683d28fbc8f4f8/langgraph_checkpoint-2.0.25.tar.gz", hash = "sha256:77a63cab7b5f84dec1d49db561326ec28bdd48bcefb7fe4ac372069d2609287b", size = 36952 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/52/bceb5b5348c7a60ef0625ab0a0a0a9ff5d78f0e12aed8cc55c49d5e8a8c9/langgraph_checkpoint-2.0.25-py3-none-any.whl", hash = "sha256:23416a0f5bc9dd712ac10918fc13e8c9c4530c419d2985a441df71a38fc81602", size = 42312 },
]
[[package]]
name = "langgraph-prebuilt"
version = "0.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph-checkpoint" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/30/f31f0e076c37d097b53e4cff5d479a3686e1991f6c86a1a4727d5d1f5489/langgraph_prebuilt-0.1.8.tar.gz", hash = "sha256:4de7659151829b2b955b6798df6800e580e617782c15c2c5b29b139697491831", size = 24543 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/72/9e092665502f8f52f2708065ed14fbbba3f95d1a1b65d62049b0c5fcdf00/langgraph_prebuilt-0.1.8-py3-none-any.whl", hash = "sha256:ae97b828ae00be2cefec503423aa782e1bff165e9b94592e224da132f2526968", size = 25903 },
]
[[package]]
name = "langgraph-sdk"
version = "0.1.69"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "orjson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/78/4ca0603240332be5fc8ebbb9bc418896310643bef32e3319a311fab37e4c/langgraph_sdk-0.1.69.tar.gz", hash = "sha256:2e85d73b78a03f9606d0fafd62048b3060371149f6f9e61f07f087fd56c766fa", size = 45343 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/e6/8e82a0373e233392d83ae37f473c9799c536b307322f0caf49a59bce9522/langgraph_sdk-0.1.69-py3-none-any.whl", hash = "sha256:0ed117bcdf67285a17c57f6265f1d94f2dbd71346cf48a8e1a5fa25e523eb6b8", size = 48905 },
]
[[package]]
name = "langsmith"
version = "0.3.33"
@@ -447,6 +507,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/9c/b66ce9245ff319df2c3278acd351a3f6145ef34b4a2d7f4b0f739368370f/orjson-3.10.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe0a145e96d51971407cb8ba947e63ead2aa915db59d6631a355f5f2150b56b7", size = 133954 },
]
[[package]]
name = "ormsgpack"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/a7/462cf8ff5e29241868b82d3a5ec124d690eb6a6a5c6fa5bb1367b839e027/ormsgpack-1.9.1.tar.gz", hash = "sha256:3da6e63d82565e590b98178545e64f0f8506137b92bd31a2d04fd7c82baf5794", size = 56887 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/32/5f504c0695ff96aaaf0452bee522d79b5a3ee809f22fd77fdb0dd5756d86/ormsgpack-1.9.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f1f804fd9c0fd84213a6022c34172f82323b34afa7052a4af18797582cf56365", size = 382793 },
{ url = "https://files.pythonhosted.org/packages/1e/c6/64fe1270271b61495611f1d3068baedb57d76e0f93ce7156f3763fb79b32/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab5cec99c46276b37071d570aab98603f3d0309b3818da3247eb64bb95e5cfc", size = 213974 },
{ url = "https://files.pythonhosted.org/packages/13/56/6666d6a9b82c7d2021fce6823ff823bc373a4e7280979c1b453317678fbc/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c12c6bb30e6df6fc0213b77f0a5e143f371d618be2e8eb4d555340ce01c6900", size = 217200 },
{ url = "https://files.pythonhosted.org/packages/5f/fb/b844ed1e69d8615163525a8403d7abd3548b3fbfa0f3a973808f36145a0f/ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994d4bbb7ee333264a3e55e30ccee063df6635d785f21a08bf52f67821454a51", size = 223648 },
{ url = "https://files.pythonhosted.org/packages/f2/26/c40c3e300f9c61a5ed7a6921656dd0d2907a8174936e1e643677585e497c/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a668a584cf4bb6e1a6ef5a35f3f0d0fdae80cfb7237344ad19a50cce8c79317b", size = 394197 },
{ url = "https://files.pythonhosted.org/packages/2d/4c/7a4ae187f18e7abf7ab0662b473264a60a5aa4e9bff266f541a8855df163/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:aaf77699203822638014c604d100f132583844d4fd01eb639a2266970c02cfdf", size = 480550 },
{ url = "https://files.pythonhosted.org/packages/b1/33/5c465dfd5571f816835bb9e371987bf081b529c64ef28a72d18b0b59902d/ormsgpack-1.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:003d7e1992b447898caf25a820b3037ec68a57864b3e2f34b64693b7d60a9984", size = 396955 },
{ url = "https://files.pythonhosted.org/packages/8f/fd/8f64f477b5c6d66e9c6343d7d3f32d7063ba20ab151dd36884e6504899ab/ormsgpack-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:67fefc77e4ba9469f79426769eb4c78acf21f22bef3ab1239a72dd728036ffc2", size = 125102 },
{ url = "https://files.pythonhosted.org/packages/d8/3b/388e7915a28db6ab3daedfd4937bd7b063c50dd1543068daa31c0a3b70ed/ormsgpack-1.9.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:16eaf32c33ab4249e242181d59e2509b8e0330d6f65c1d8bf08c3dea38fd7c02", size = 382794 },
{ url = "https://files.pythonhosted.org/packages/0f/b4/3f4afba058822bf69b274e0defe507056be0340e65363c3ebcd312b01b84/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c70f2e5b2f9975536e8f7936a9721601dc54febe363d2d82f74c9b31d4fe1c65", size = 213974 },
{ url = "https://files.pythonhosted.org/packages/bf/be/f0e21366d51b6e28fc3a55425be6a125545370d3479bf25be081e83ee236/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:17c9e18b07d69e3db2e0f8af4731040175e11bdfde78ad8e28126e9e66ec5167", size = 217200 },
{ url = "https://files.pythonhosted.org/packages/cc/90/67a23c1c880a6e5552acb45f9555b642528f89c8bcf75283a2ea64ef7175/ormsgpack-1.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73538d749096bb6470328601a2be8f7bdec28849ec6fd19595c232a5848d7124", size = 223649 },
{ url = "https://files.pythonhosted.org/packages/80/ad/116c1f970b5b4453e4faa52645517a2e5eaf1ab385ba09a5c54253d07d0e/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:827ff71de228cfd6d07b9d6b47911aa61b1e8dc995dec3caf8fdcdf4f874bcd0", size = 394200 },
{ url = "https://files.pythonhosted.org/packages/c5/a2/b224a5ef193628a15205e473179276b87e8290d321693e4934a05cbd6ccf/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7307f808b3df282c8e8ed92c6ebceeb3eea3d8eeec808438f3f212226b25e217", size = 480551 },
{ url = "https://files.pythonhosted.org/packages/b5/f4/a0f528196af6ab46e6c3f3051cf7403016bdc7b7d3e673ea5b04b145be98/ormsgpack-1.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f30aad7fb083bed1c540a3c163c6a9f63a94e3c538860bf8f13386c29b560ad5", size = 396959 },
{ url = "https://files.pythonhosted.org/packages/bc/6b/60c6f4787e3e93f5eb34fccb163753a8771465983a579e3405152f2422fd/ormsgpack-1.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:829a1b4c5bc3c38ece0c55cf91ebc09c3b987fceb24d3f680c2bcd03fd3789a4", size = 125100 },
{ url = "https://files.pythonhosted.org/packages/dd/f1/155a598cc8030526ccaaf91ba4d61530f87900645559487edba58b0a90a2/ormsgpack-1.9.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1ede445fc3fdba219bb0e0d1f289df26a9c7602016b7daac6fafe8fe4e91548f", size = 383225 },
{ url = "https://files.pythonhosted.org/packages/23/1c/ef3097ba550fad55c79525f461febdd4e0d9cc18d065248044536f09488e/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db50b9f918e25b289114312ed775794d0978b469831b992bdc65bfe20b91fe30", size = 214056 },
{ url = "https://files.pythonhosted.org/packages/27/77/64d0da25896b2cbb99505ca518c109d7dd1964d7fde14c10943731738b60/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c7d8fc58e4333308f58ec720b1ee6b12b2b3fe2d2d8f0766ab751cb351e8757", size = 217339 },
{ url = "https://files.pythonhosted.org/packages/6c/10/c3a7fd0a0068b0bb52cccbfeb5656db895d69e895a3abbc210c4b3f98ff8/ormsgpack-1.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeee6d08c040db265cb8563444aba343ecb32cbdbe2414a489dcead9f70c6765", size = 223816 },
{ url = "https://files.pythonhosted.org/packages/43/e7/aee1238dba652f2116c2523d36fd1c5f9775436032be5c233108fd2a1415/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fbb8181c198bdc413a4e889e5200f010724eea4b6d5a9a7eee2df039ac04aca", size = 394287 },
{ url = "https://files.pythonhosted.org/packages/c7/09/1b452a92376f29d7a2da7c18fb01cf09978197a8eccbb8b204e72fd5a970/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:16488f094ac0e2250cceea6caf72962614aa432ee11dd57ef45e1ad25ece3eff", size = 480709 },
{ url = "https://files.pythonhosted.org/packages/de/13/7fa9fee5a73af8a73a42bf8c2e69489605714f65f5a41454400a05e84a3b/ormsgpack-1.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:422d960bfd6ad88be20794f50ec7953d8f7a0f2df60e19d0e8feb994e2ed64ee", size = 397247 },
{ url = "https://files.pythonhosted.org/packages/a1/2d/2e87cb28110db0d3bb750edd4d8719b5068852a2eef5e96b0bf376bb8a81/ormsgpack-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:e6e2f9eab527cf43fb4a4293e493370276b1c8716cf305689202d646c6a782ef", size = 125368 },
{ url = "https://files.pythonhosted.org/packages/b8/54/0390d5d092831e4df29dbafe32402891fc14b3e6ffe5a644b16cbbc9d9bc/ormsgpack-1.9.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ac61c18d9dd085e8519b949f7e655f7fb07909fd09c53b4338dd33309012e289", size = 383226 },
{ url = "https://files.pythonhosted.org/packages/47/64/8b15d262d1caefead8fb22ec144f5ff7d9505fc31c22bc34598053d46fbe/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134840b8c6615da2c24ce77bd12a46098015c808197a9995c7a2d991e1904eec", size = 214057 },
{ url = "https://files.pythonhosted.org/packages/57/00/65823609266bad4d5ed29ea753d24a3bdb01c7edaf923da80967fc31f9c5/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38fd42618f626394b2c7713c5d4bcbc917254e9753d5d4cde460658b51b11a74", size = 217340 },
{ url = "https://files.pythonhosted.org/packages/a0/51/e535c50f7f87b49110233647f55300d7975139ef5e51f1adb4c55f58c124/ormsgpack-1.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d36397333ad07b9eba4c2e271fa78951bd81afc059c85a6e9f6c0eb2de07cda", size = 223815 },
{ url = "https://files.pythonhosted.org/packages/0c/ee/393e4a6de2a62124bf589602648f295a9fb3907a0e2fe80061b88899d072/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:603063089597917d04e4c1b1d53988a34f7dc2ff1a03adcfd1cf4ae966d5fba6", size = 394287 },
{ url = "https://files.pythonhosted.org/packages/c6/d8/e56d7c3cb73a0e533e3e2a21ae5838b2aa36a9dac1ca9c861af6bae5a369/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:94bbf2b185e0cb721ceaba20e64b7158e6caf0cecd140ca29b9f05a8d5e91e2f", size = 480707 },
{ url = "https://files.pythonhosted.org/packages/e6/e0/6a3c6a6dc98583a721c54b02f5195bde8f801aebdeda9b601fa2ab30ad39/ormsgpack-1.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38f380b1e8c96a712eb302b9349347385161a8e29046868ae2bfdfcb23e2692", size = 397246 },
{ url = "https://files.pythonhosted.org/packages/b0/60/0ee5d790f13507e1f75ac21fc82dc1ef29afe1f520bd0f249d65b2f4839b/ormsgpack-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:a4bc63fb30db94075611cedbbc3d261dd17cf2aa8ff75a0fd684cd45ca29cb1b", size = 125371 },
]
[[package]]
name = "packaging"
version = "24.2"
@@ -837,6 +937,79 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
]
[[package]]
name = "xxhash"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/5e/d6e5258d69df8b4ed8c83b6664f2b47d30d2dec551a29ad72a6c69eafd31/xxhash-3.5.0.tar.gz", hash = "sha256:84f2caddf951c9cbf8dc2e22a89d4ccf5d86391ac6418fe81e3c67d0cf60b45f", size = 84241 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/8a/0e9feca390d512d293afd844d31670e25608c4a901e10202aa98785eab09/xxhash-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ece616532c499ee9afbb83078b1b952beffef121d989841f7f4b3dc5ac0fd212", size = 31970 },
{ url = "https://files.pythonhosted.org/packages/16/e6/be5aa49580cd064a18200ab78e29b88b1127e1a8c7955eb8ecf81f2626eb/xxhash-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3171f693dbc2cef6477054a665dc255d996646b4023fe56cb4db80e26f4cc520", size = 30801 },
{ url = "https://files.pythonhosted.org/packages/20/ee/b8a99ebbc6d1113b3a3f09e747fa318c3cde5b04bd9c197688fadf0eeae8/xxhash-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5d3e570ef46adaf93fc81b44aca6002b5a4d8ca11bd0580c07eac537f36680", size = 220927 },
{ url = "https://files.pythonhosted.org/packages/58/62/15d10582ef159283a5c2b47f6d799fc3303fe3911d5bb0bcc820e1ef7ff4/xxhash-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cb29a034301e2982df8b1fe6328a84f4b676106a13e9135a0d7e0c3e9f806da", size = 200360 },
{ url = "https://files.pythonhosted.org/packages/23/41/61202663ea9b1bd8e53673b8ec9e2619989353dba8cfb68e59a9cbd9ffe3/xxhash-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0d307d27099bb0cbeea7260eb39ed4fdb99c5542e21e94bb6fd29e49c57a23", size = 428528 },
{ url = "https://files.pythonhosted.org/packages/f2/07/d9a3059f702dec5b3b703737afb6dda32f304f6e9da181a229dafd052c29/xxhash-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0342aafd421795d740e514bc9858ebddfc705a75a8c5046ac56d85fe97bf196", size = 194149 },
{ url = "https://files.pythonhosted.org/packages/eb/58/27caadf78226ecf1d62dbd0c01d152ed381c14c1ee4ad01f0d460fc40eac/xxhash-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dbbd9892c5ebffeca1ed620cf0ade13eb55a0d8c84e0751a6653adc6ac40d0c", size = 207703 },
{ url = "https://files.pythonhosted.org/packages/b1/08/32d558ce23e1e068453c39aed7b3c1cdc690c177873ec0ca3a90d5808765/xxhash-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4cc2d67fdb4d057730c75a64c5923abfa17775ae234a71b0200346bfb0a7f482", size = 216255 },
{ url = "https://files.pythonhosted.org/packages/3f/d4/2b971e2d2b0a61045f842b622ef11e94096cf1f12cd448b6fd426e80e0e2/xxhash-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ec28adb204b759306a3d64358a5e5c07d7b1dd0ccbce04aa76cb9377b7b70296", size = 202744 },
{ url = "https://files.pythonhosted.org/packages/19/ae/6a6438864a8c4c39915d7b65effd85392ebe22710412902487e51769146d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1328f6d8cca2b86acb14104e381225a3d7b42c92c4b86ceae814e5c400dbb415", size = 210115 },
{ url = "https://files.pythonhosted.org/packages/48/7d/b3c27c27d1fc868094d02fe4498ccce8cec9fcc591825c01d6bcb0b4fc49/xxhash-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8d47ebd9f5d9607fd039c1fbf4994e3b071ea23eff42f4ecef246ab2b7334198", size = 414247 },
{ url = "https://files.pythonhosted.org/packages/a1/05/918f9e7d2fbbd334b829997045d341d6239b563c44e683b9a7ef8fe50f5d/xxhash-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b96d559e0fcddd3343c510a0fe2b127fbff16bf346dd76280b82292567523442", size = 191419 },
{ url = "https://files.pythonhosted.org/packages/08/29/dfe393805b2f86bfc47c290b275f0b7c189dc2f4e136fd4754f32eb18a8d/xxhash-3.5.0-cp310-cp310-win32.whl", hash = "sha256:61c722ed8d49ac9bc26c7071eeaa1f6ff24053d553146d5df031802deffd03da", size = 30114 },
{ url = "https://files.pythonhosted.org/packages/7b/d7/aa0b22c4ebb7c3ccb993d4c565132abc641cd11164f8952d89eb6a501909/xxhash-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9bed5144c6923cc902cd14bb8963f2d5e034def4486ab0bbe1f58f03f042f9a9", size = 30003 },
{ url = "https://files.pythonhosted.org/packages/69/12/f969b81541ee91b55f1ce469d7ab55079593c80d04fd01691b550e535000/xxhash-3.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:893074d651cf25c1cc14e3bea4fceefd67f2921b1bb8e40fcfeba56820de80c6", size = 26773 },
{ url = "https://files.pythonhosted.org/packages/b8/c7/afed0f131fbda960ff15eee7f304fa0eeb2d58770fade99897984852ef23/xxhash-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02c2e816896dc6f85922ced60097bcf6f008dedfc5073dcba32f9c8dd786f3c1", size = 31969 },
{ url = "https://files.pythonhosted.org/packages/8c/0c/7c3bc6d87e5235672fcc2fb42fd5ad79fe1033925f71bf549ee068c7d1ca/xxhash-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6027dcd885e21581e46d3c7f682cfb2b870942feeed58a21c29583512c3f09f8", size = 30800 },
{ url = "https://files.pythonhosted.org/packages/04/9e/01067981d98069eec1c20201f8c145367698e9056f8bc295346e4ea32dd1/xxhash-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1308fa542bbdbf2fa85e9e66b1077eea3a88bef38ee8a06270b4298a7a62a166", size = 221566 },
{ url = "https://files.pythonhosted.org/packages/d4/09/d4996de4059c3ce5342b6e1e6a77c9d6c91acce31f6ed979891872dd162b/xxhash-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28b2fdcee797e1c1961cd3bcd3d545cab22ad202c846235197935e1df2f8ef7", size = 201214 },
{ url = "https://files.pythonhosted.org/packages/62/f5/6d2dc9f8d55a7ce0f5e7bfef916e67536f01b85d32a9fbf137d4cadbee38/xxhash-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:924361811732ddad75ff23e90efd9ccfda4f664132feecb90895bade6a1b4623", size = 429433 },
{ url = "https://files.pythonhosted.org/packages/d9/72/9256303f10e41ab004799a4aa74b80b3c5977d6383ae4550548b24bd1971/xxhash-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89997aa1c4b6a5b1e5b588979d1da048a3c6f15e55c11d117a56b75c84531f5a", size = 194822 },
{ url = "https://files.pythonhosted.org/packages/34/92/1a3a29acd08248a34b0e6a94f4e0ed9b8379a4ff471f1668e4dce7bdbaa8/xxhash-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c4f4e8c59837de103344eb1c8a3851f670309eb5c361f746805c5471b8c88", size = 208538 },
{ url = "https://files.pythonhosted.org/packages/53/ad/7fa1a109663366de42f724a1cdb8e796a260dbac45047bce153bc1e18abf/xxhash-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbd2ecfbfee70bc1a4acb7461fa6af7748ec2ab08ac0fa298f281c51518f982c", size = 216953 },
{ url = "https://files.pythonhosted.org/packages/35/02/137300e24203bf2b2a49b48ce898ecce6fd01789c0fcd9c686c0a002d129/xxhash-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25b5a51dc3dfb20a10833c8eee25903fd2e14059e9afcd329c9da20609a307b2", size = 203594 },
{ url = "https://files.pythonhosted.org/packages/23/03/aeceb273933d7eee248c4322b98b8e971f06cc3880e5f7602c94e5578af5/xxhash-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a8fb786fb754ef6ff8c120cb96629fb518f8eb5a61a16aac3a979a9dbd40a084", size = 210971 },
{ url = "https://files.pythonhosted.org/packages/e3/64/ed82ec09489474cbb35c716b189ddc1521d8b3de12b1b5ab41ce7f70253c/xxhash-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a905ad00ad1e1c34fe4e9d7c1d949ab09c6fa90c919860c1534ff479f40fd12d", size = 415050 },
{ url = "https://files.pythonhosted.org/packages/71/43/6db4c02dcb488ad4e03bc86d70506c3d40a384ee73c9b5c93338eb1f3c23/xxhash-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:963be41bcd49f53af6d795f65c0da9b4cc518c0dd9c47145c98f61cb464f4839", size = 192216 },
{ url = "https://files.pythonhosted.org/packages/22/6d/db4abec29e7a567455344433d095fdb39c97db6955bb4a2c432e486b4d28/xxhash-3.5.0-cp311-cp311-win32.whl", hash = "sha256:109b436096d0a2dd039c355fa3414160ec4d843dfecc64a14077332a00aeb7da", size = 30120 },
{ url = "https://files.pythonhosted.org/packages/52/1c/fa3b61c0cf03e1da4767213672efe186b1dfa4fc901a4a694fb184a513d1/xxhash-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b702f806693201ad6c0a05ddbbe4c8f359626d0b3305f766077d51388a6bac58", size = 30003 },
{ url = "https://files.pythonhosted.org/packages/6b/8e/9e6fc572acf6e1cc7ccb01973c213f895cb8668a9d4c2b58a99350da14b7/xxhash-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:c4dcb4120d0cc3cc448624147dba64e9021b278c63e34a38789b688fd0da9bf3", size = 26777 },
{ url = "https://files.pythonhosted.org/packages/07/0e/1bfce2502c57d7e2e787600b31c83535af83746885aa1a5f153d8c8059d6/xxhash-3.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14470ace8bd3b5d51318782cd94e6f94431974f16cb3b8dc15d52f3b69df8e00", size = 31969 },
{ url = "https://files.pythonhosted.org/packages/3f/d6/8ca450d6fe5b71ce521b4e5db69622383d039e2b253e9b2f24f93265b52c/xxhash-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59aa1203de1cb96dbeab595ded0ad0c0056bb2245ae11fac11c0ceea861382b9", size = 30787 },
{ url = "https://files.pythonhosted.org/packages/5b/84/de7c89bc6ef63d750159086a6ada6416cc4349eab23f76ab870407178b93/xxhash-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08424f6648526076e28fae6ea2806c0a7d504b9ef05ae61d196d571e5c879c84", size = 220959 },
{ url = "https://files.pythonhosted.org/packages/fe/86/51258d3e8a8545ff26468c977101964c14d56a8a37f5835bc0082426c672/xxhash-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61a1ff00674879725b194695e17f23d3248998b843eb5e933007ca743310f793", size = 200006 },
{ url = "https://files.pythonhosted.org/packages/02/0a/96973bd325412feccf23cf3680fd2246aebf4b789122f938d5557c54a6b2/xxhash-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f2c61bee5844d41c3eb015ac652a0229e901074951ae48581d58bfb2ba01be", size = 428326 },
{ url = "https://files.pythonhosted.org/packages/11/a7/81dba5010f7e733de88af9555725146fc133be97ce36533867f4c7e75066/xxhash-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d32a592cac88d18cc09a89172e1c32d7f2a6e516c3dfde1b9adb90ab5df54a6", size = 194380 },
{ url = "https://files.pythonhosted.org/packages/fb/7d/f29006ab398a173f4501c0e4977ba288f1c621d878ec217b4ff516810c04/xxhash-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70dabf941dede727cca579e8c205e61121afc9b28516752fd65724be1355cc90", size = 207934 },
{ url = "https://files.pythonhosted.org/packages/8a/6e/6e88b8f24612510e73d4d70d9b0c7dff62a2e78451b9f0d042a5462c8d03/xxhash-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e5d0ddaca65ecca9c10dcf01730165fd858533d0be84c75c327487c37a906a27", size = 216301 },
{ url = "https://files.pythonhosted.org/packages/af/51/7862f4fa4b75a25c3b4163c8a873f070532fe5f2d3f9b3fc869c8337a398/xxhash-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e5b5e16c5a480fe5f59f56c30abdeba09ffd75da8d13f6b9b6fd224d0b4d0a2", size = 203351 },
{ url = "https://files.pythonhosted.org/packages/22/61/8d6a40f288f791cf79ed5bb113159abf0c81d6efb86e734334f698eb4c59/xxhash-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149b7914451eb154b3dfaa721315117ea1dac2cc55a01bfbd4df7c68c5dd683d", size = 210294 },
{ url = "https://files.pythonhosted.org/packages/17/02/215c4698955762d45a8158117190261b2dbefe9ae7e5b906768c09d8bc74/xxhash-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:eade977f5c96c677035ff39c56ac74d851b1cca7d607ab3d8f23c6b859379cab", size = 414674 },
{ url = "https://files.pythonhosted.org/packages/31/5c/b7a8db8a3237cff3d535261325d95de509f6a8ae439a5a7a4ffcff478189/xxhash-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa9f547bd98f5553d03160967866a71056a60960be00356a15ecc44efb40ba8e", size = 192022 },
{ url = "https://files.pythonhosted.org/packages/78/e3/dd76659b2811b3fd06892a8beb850e1996b63e9235af5a86ea348f053e9e/xxhash-3.5.0-cp312-cp312-win32.whl", hash = "sha256:f7b58d1fd3551b8c80a971199543379be1cee3d0d409e1f6d8b01c1a2eebf1f8", size = 30170 },
{ url = "https://files.pythonhosted.org/packages/d9/6b/1c443fe6cfeb4ad1dcf231cdec96eb94fb43d6498b4469ed8b51f8b59a37/xxhash-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:fa0cafd3a2af231b4e113fba24a65d7922af91aeb23774a8b78228e6cd785e3e", size = 30040 },
{ url = "https://files.pythonhosted.org/packages/0f/eb/04405305f290173acc0350eba6d2f1a794b57925df0398861a20fbafa415/xxhash-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:586886c7e89cb9828bcd8a5686b12e161368e0064d040e225e72607b43858ba2", size = 26796 },
{ url = "https://files.pythonhosted.org/packages/c9/b8/e4b3ad92d249be5c83fa72916c9091b0965cb0faeff05d9a0a3870ae6bff/xxhash-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37889a0d13b0b7d739cfc128b1c902f04e32de17b33d74b637ad42f1c55101f6", size = 31795 },
{ url = "https://files.pythonhosted.org/packages/fc/d8/b3627a0aebfbfa4c12a41e22af3742cf08c8ea84f5cc3367b5de2d039cce/xxhash-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97a662338797c660178e682f3bc180277b9569a59abfb5925e8620fba00b9fc5", size = 30792 },
{ url = "https://files.pythonhosted.org/packages/c3/cc/762312960691da989c7cd0545cb120ba2a4148741c6ba458aa723c00a3f8/xxhash-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f85e0108d51092bdda90672476c7d909c04ada6923c14ff9d913c4f7dc8a3bc", size = 220950 },
{ url = "https://files.pythonhosted.org/packages/fe/e9/cc266f1042c3c13750e86a535496b58beb12bf8c50a915c336136f6168dc/xxhash-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2fd827b0ba763ac919440042302315c564fdb797294d86e8cdd4578e3bc7f3", size = 199980 },
{ url = "https://files.pythonhosted.org/packages/bf/85/a836cd0dc5cc20376de26b346858d0ac9656f8f730998ca4324921a010b9/xxhash-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82085c2abec437abebf457c1d12fccb30cc8b3774a0814872511f0f0562c768c", size = 428324 },
{ url = "https://files.pythonhosted.org/packages/b4/0e/15c243775342ce840b9ba34aceace06a1148fa1630cd8ca269e3223987f5/xxhash-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fda5de378626e502b42b311b049848c2ef38784d0d67b6f30bb5008642f8eb", size = 194370 },
{ url = "https://files.pythonhosted.org/packages/87/a1/b028bb02636dfdc190da01951d0703b3d904301ed0ef6094d948983bef0e/xxhash-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c279f0d2b34ef15f922b77966640ade58b4ccdfef1c4d94b20f2a364617a493f", size = 207911 },
{ url = "https://files.pythonhosted.org/packages/80/d5/73c73b03fc0ac73dacf069fdf6036c9abad82de0a47549e9912c955ab449/xxhash-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:89e66ceed67b213dec5a773e2f7a9e8c58f64daeb38c7859d8815d2c89f39ad7", size = 216352 },
{ url = "https://files.pythonhosted.org/packages/b6/2a/5043dba5ddbe35b4fe6ea0a111280ad9c3d4ba477dd0f2d1fe1129bda9d0/xxhash-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bcd51708a633410737111e998ceb3b45d3dbc98c0931f743d9bb0a209033a326", size = 203410 },
{ url = "https://files.pythonhosted.org/packages/a2/b2/9a8ded888b7b190aed75b484eb5c853ddd48aa2896e7b59bbfbce442f0a1/xxhash-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ff2c0a34eae7df88c868be53a8dd56fbdf592109e21d4bfa092a27b0bf4a7bf", size = 210322 },
{ url = "https://files.pythonhosted.org/packages/98/62/440083fafbc917bf3e4b67c2ade621920dd905517e85631c10aac955c1d2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e28503dccc7d32e0b9817aa0cbfc1f45f563b2c995b7a66c4c8a0d232e840c7", size = 414725 },
{ url = "https://files.pythonhosted.org/packages/75/db/009206f7076ad60a517e016bb0058381d96a007ce3f79fa91d3010f49cc2/xxhash-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6c50017518329ed65a9e4829154626f008916d36295b6a3ba336e2458824c8c", size = 192070 },
{ url = "https://files.pythonhosted.org/packages/1f/6d/c61e0668943a034abc3a569cdc5aeae37d686d9da7e39cf2ed621d533e36/xxhash-3.5.0-cp313-cp313-win32.whl", hash = "sha256:53a068fe70301ec30d868ece566ac90d873e3bb059cf83c32e76012c889b8637", size = 30172 },
{ url = "https://files.pythonhosted.org/packages/96/14/8416dce965f35e3d24722cdf79361ae154fa23e2ab730e5323aa98d7919e/xxhash-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:80babcc30e7a1a484eab952d76a4f4673ff601f54d5142c26826502740e70b43", size = 30041 },
{ url = "https://files.pythonhosted.org/packages/27/ee/518b72faa2073f5aa8e3262408d284892cb79cf2754ba0c3a5870645ef73/xxhash-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:4811336f1ce11cac89dcbd18f3a25c527c16311709a89313c3acaf771def2d4b", size = 26801 },
{ url = "https://files.pythonhosted.org/packages/ab/9a/233606bada5bd6f50b2b72c45de3d9868ad551e83893d2ac86dc7bb8553a/xxhash-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2014c5b3ff15e64feecb6b713af12093f75b7926049e26a580e94dcad3c73d8c", size = 29732 },
{ url = "https://files.pythonhosted.org/packages/0c/67/f75276ca39e2c6604e3bee6c84e9db8a56a4973fde9bf35989787cf6e8aa/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fab81ef75003eda96239a23eda4e4543cedc22e34c373edcaf744e721a163986", size = 36214 },
{ url = "https://files.pythonhosted.org/packages/0f/f8/f6c61fd794229cc3848d144f73754a0c107854372d7261419dcbbd286299/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2febf914ace002132aa09169cc572e0d8959d0f305f93d5828c4836f9bc5a6", size = 32020 },
{ url = "https://files.pythonhosted.org/packages/79/d3/c029c99801526f859e6b38d34ab87c08993bf3dcea34b11275775001638a/xxhash-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d3a10609c51da2a1c0ea0293fc3968ca0a18bd73838455b5bca3069d7f8e32b", size = 40515 },
{ url = "https://files.pythonhosted.org/packages/62/e3/bef7b82c1997579c94de9ac5ea7626d01ae5858aa22bf4fcb38bf220cb3e/xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da", size = 30064 },
]
[[package]]
name = "zstandard"
version = "0.23.0"