mirror of
https://github.com/langchain-ai/langchain-sandbox.git
synced 2026-07-01 14:02:10 -04:00
allow returning session bytes & metadata directly (#12)
This commit is contained in:
@@ -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,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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
Generated
+174
-1
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user