withruntime 0.3.1__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- withruntime-0.5.0/PKG-INFO +135 -0
- withruntime-0.5.0/README.md +124 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/pyproject.toml +7 -1
- withruntime-0.5.0/tests/test_audit.py +70 -0
- withruntime-0.5.0/tests/test_capacity_wait.py +133 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/tests/test_client.py +60 -0
- withruntime-0.5.0/tests/test_daytona.py +363 -0
- withruntime-0.5.0/tests/test_deepagents.py +79 -0
- withruntime-0.5.0/tests/test_e2b_code_interpreter.py +130 -0
- withruntime-0.5.0/tests/test_e2b_sandbox.py +415 -0
- withruntime-0.5.0/tests/test_images_v2.py +137 -0
- withruntime-0.5.0/tests/test_lifecycle_settings.py +135 -0
- withruntime-0.5.0/tests/test_observability.py +134 -0
- withruntime-0.5.0/tests/test_openai_agents.py +456 -0
- withruntime-0.5.0/tests/test_proxy.py +267 -0
- withruntime-0.5.0/tests/test_secrets.py +91 -0
- withruntime-0.5.0/tests/test_switching.py +79 -0
- withruntime-0.5.0/tests/test_tools.py +115 -0
- withruntime-0.5.0/tests/test_vercel.py +282 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/__init__.py +10 -1
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_client.py +160 -19
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/__init__.py +11 -0
- withruntime-0.5.0/withruntime/_async_products/audit.py +26 -0
- withruntime-0.5.0/withruntime/_async_products/images.py +379 -0
- withruntime-0.5.0/withruntime/_async_products/observability.py +128 -0
- withruntime-0.5.0/withruntime/_async_products/secrets.py +38 -0
- withruntime-0.5.0/withruntime/_async_products/switching.py +36 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_clock.py +15 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_errors.py +21 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_http.py +28 -7
- withruntime-0.5.0/withruntime/_proxy.py +177 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_client.py +160 -19
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/__init__.py +11 -0
- withruntime-0.5.0/withruntime/_sync_products/audit.py +27 -0
- withruntime-0.5.0/withruntime/_sync_products/images.py +380 -0
- withruntime-0.5.0/withruntime/_sync_products/observability.py +129 -0
- withruntime-0.5.0/withruntime/_sync_products/secrets.py +39 -0
- withruntime-0.5.0/withruntime/_sync_products/switching.py +37 -0
- withruntime-0.5.0/withruntime/_tunnel.py +330 -0
- withruntime-0.5.0/withruntime/_version.py +1 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_ws.py +6 -5
- withruntime-0.5.0/withruntime/daytona/__init__.py +74 -0
- withruntime-0.5.0/withruntime/daytona/_async_daytona.py +1029 -0
- withruntime-0.5.0/withruntime/daytona/_async_io.py +34 -0
- withruntime-0.5.0/withruntime/daytona/_core.py +917 -0
- withruntime-0.5.0/withruntime/daytona/_sync_daytona.py +1027 -0
- withruntime-0.5.0/withruntime/daytona/_sync_io.py +43 -0
- withruntime-0.5.0/withruntime/deepagents.py +131 -0
- withruntime-0.5.0/withruntime/e2b/__init__.py +57 -0
- withruntime-0.5.0/withruntime/e2b/_async_io.py +54 -0
- withruntime-0.5.0/withruntime/e2b/_async_sandbox.py +682 -0
- withruntime-0.5.0/withruntime/e2b/_core.py +509 -0
- withruntime-0.5.0/withruntime/e2b/_sync_io.py +41 -0
- withruntime-0.5.0/withruntime/e2b/_sync_sandbox.py +681 -0
- withruntime-0.5.0/withruntime/e2b/code_interpreter/__init__.py +14 -0
- withruntime-0.5.0/withruntime/e2b/code_interpreter/_async_ci.py +143 -0
- withruntime-0.5.0/withruntime/e2b/code_interpreter/_models.py +101 -0
- withruntime-0.5.0/withruntime/e2b/code_interpreter/_sync_ci.py +142 -0
- withruntime-0.5.0/withruntime/openai_agents.py +844 -0
- withruntime-0.5.0/withruntime/tools.py +201 -0
- withruntime-0.5.0/withruntime/vercel/__init__.py +12 -0
- withruntime-0.5.0/withruntime/vercel/_async_io.py +58 -0
- withruntime-0.5.0/withruntime/vercel/_async_sandbox.py +862 -0
- withruntime-0.5.0/withruntime/vercel/_core.py +438 -0
- withruntime-0.5.0/withruntime/vercel/_sync_io.py +51 -0
- withruntime-0.5.0/withruntime/vercel/_sync_sandbox.py +860 -0
- withruntime-0.5.0/withruntime/vercel/api.py +32 -0
- withruntime-0.5.0/withruntime/vercel/sandbox/__init__.py +56 -0
- withruntime-0.5.0/withruntime/vercel/sandbox/sync.py +26 -0
- withruntime-0.5.0/withruntime/webhooks.py +56 -0
- withruntime-0.5.0/withruntime.egg-info/PKG-INFO +135 -0
- withruntime-0.5.0/withruntime.egg-info/SOURCES.txt +93 -0
- withruntime-0.5.0/withruntime.egg-info/requires.txt +6 -0
- withruntime-0.3.1/PKG-INFO +0 -61
- withruntime-0.3.1/README.md +0 -54
- withruntime-0.3.1/withruntime/_async_products/images.py +0 -119
- withruntime-0.3.1/withruntime/_sync_products/images.py +0 -120
- withruntime-0.3.1/withruntime/_version.py +0 -1
- withruntime-0.3.1/withruntime.egg-info/PKG-INFO +0 -61
- withruntime-0.3.1/withruntime.egg-info/SOURCES.txt +0 -38
- {withruntime-0.3.1 → withruntime-0.5.0}/setup.cfg +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/tests/test_images_volumes_interpreter.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/tests/test_limits.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/tests/test_previews_network_desktop.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/tests/test_referrals.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/desktop.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/interpreter.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/limits.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/network.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/previews.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/referrals.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_async_products/volumes.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_connection.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/desktop.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/interpreter.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/limits.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/network.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/previews.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/referrals.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime/_sync_products/volumes.py +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime.egg-info/dependency_links.txt +0 -0
- {withruntime-0.3.1 → withruntime-0.5.0}/withruntime.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: withruntime
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: openai-agents
|
|
8
|
+
Requires-Dist: openai-agents>=0.22.3; extra == "openai-agents"
|
|
9
|
+
Provides-Extra: deepagents
|
|
10
|
+
Requires-Dist: deepagents>=0.7; extra == "deepagents"
|
|
11
|
+
|
|
12
|
+
# Runtime Cloud Python SDK
|
|
13
|
+
|
|
14
|
+
One client for every Runtime Cloud product, sync and async. Python 3.10 or
|
|
15
|
+
later, standard library only.
|
|
16
|
+
|
|
17
|
+
```bash no-run
|
|
18
|
+
pip install withruntime
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The client uses `RUNTIME_API_KEY` when it is set, and otherwise the connection
|
|
22
|
+
this machine saved when `npx withruntime login` connected it (one browser
|
|
23
|
+
approval, no key to copy). On a server or a CI runner, set `RUNTIME_API_KEY`
|
|
24
|
+
from your secret manager: `npx withruntime keys create` prints a new key once
|
|
25
|
+
after an owner, admin or developer of the account approves it in the browser, or create one at
|
|
26
|
+
https://withruntime.com/account/keys. Never put a key in source code, a URL or a
|
|
27
|
+
command-line argument.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from withruntime import Sandbox
|
|
31
|
+
|
|
32
|
+
with Sandbox.create() as sbx:
|
|
33
|
+
result = sbx.exec("python3 -c 'print(6 * 7)'")
|
|
34
|
+
print(result.exit_code, result.stdout)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`Sandbox.create()` needs no arguments and returns once the sandbox is running;
|
|
38
|
+
leaving the `with` block stops it. With no arguments you get the free trial
|
|
39
|
+
while it lasts: 50 free hours, no card, up to eight sandboxes running at once.
|
|
40
|
+
`AsyncRuntime` is the same client for asyncio, method for method:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from withruntime import AsyncRuntime
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
async with AsyncRuntime() as runtime:
|
|
49
|
+
async with await runtime.sandboxes.create() as sbx:
|
|
50
|
+
print((await sbx.exec("uname -a")).stdout)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
asyncio.run(main())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `forward_port` (any
|
|
57
|
+
TCP port in the sandbox on a local port), `files` (read, write,
|
|
58
|
+
list, glob, stat, move, remove, upload and download directories), `pause`,
|
|
59
|
+
`wake`, `extend`, `update`, `keep_alive`, `fork` and `snapshot`, and the
|
|
60
|
+
`interpreter`, `network`, `previews` and `desktop` products. A paused sandbox
|
|
61
|
+
also wakes by itself on the next call, and `Sandbox.get_or_create(name)` returns
|
|
62
|
+
the sandbox with that name or creates it. The client has `sandboxes`, `images`,
|
|
63
|
+
`volumes`, `snapshots`, `limits`, `feedback` and `support`;
|
|
64
|
+
`runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
|
|
65
|
+
what its agent may still spend today. Every write carries an
|
|
66
|
+
idempotency key, so retries never do anything twice; errors are typed and carry
|
|
67
|
+
`code`, `hint` and `request_id`.
|
|
68
|
+
|
|
69
|
+
## Behind a proxy
|
|
70
|
+
|
|
71
|
+
The client reads `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` (upper or lower
|
|
72
|
+
case; lower wins when both are set), sync and async. Calls to the API and
|
|
73
|
+
terminal WebSockets go through `HTTPS_PROXY` as a CONNECT tunnel, with any
|
|
74
|
+
`user:password@` in its address sent to the proxy. `HTTP_PROXY` is used only
|
|
75
|
+
for `http://` addresses, never for the HTTPS API. `NO_PROXY` lists hosts to
|
|
76
|
+
reach directly, split by commas or spaces: a name covers its subdomains, a
|
|
77
|
+
leading dot is allowed, `host:port` limits it to one port, and `*` means every
|
|
78
|
+
host. A proxy address without a scheme is `http://`; only `http://` proxies are
|
|
79
|
+
supported, and anything else fails at once with `invalid_proxy`. When a call
|
|
80
|
+
cannot get through, the error names the proxy it tried (without the password),
|
|
81
|
+
for example `No answer from Runtime at https://api.withruntime.com through the
|
|
82
|
+
proxy http://proxy.internal:3128 (HTTPS_PROXY).`
|
|
83
|
+
|
|
84
|
+
## Code written for E2B
|
|
85
|
+
|
|
86
|
+
`withruntime.e2b` runs code written for E2B's Python SDK on Runtime. Change
|
|
87
|
+
the import and set `RUNTIME_API_KEY`:
|
|
88
|
+
|
|
89
|
+
```python no-run
|
|
90
|
+
from withruntime.e2b import Sandbox, AsyncSandbox # was: from e2b import ...
|
|
91
|
+
from withruntime.e2b.code_interpreter import Sandbox # was: from e2b_code_interpreter import Sandbox
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Sandboxes get E2B's defaults: 2 vCPU, 512 MiB and a 300-second timeout.
|
|
95
|
+
Timeouts are in seconds, as in E2B's Python SDK. `runtime_create={...}` passes
|
|
96
|
+
Runtime's own create fields, for example `{"funding": "trial"}`. What Runtime
|
|
97
|
+
does not do the way E2B does raises `NotSupportedException` before anything
|
|
98
|
+
happens, naming what to use instead. Importing `withruntime` alone does not
|
|
99
|
+
load it. The sync `Sandbox` is generated from the async one by
|
|
100
|
+
`scripts/generate_e2b_sync.py`.
|
|
101
|
+
|
|
102
|
+
## Code written for Daytona or Vercel Sandbox
|
|
103
|
+
|
|
104
|
+
`withruntime.daytona` and `withruntime.vercel` do the same for Daytona's
|
|
105
|
+
Python SDK and Vercel Sandbox's. Change the import:
|
|
106
|
+
|
|
107
|
+
```python no-run
|
|
108
|
+
from withruntime.daytona import Daytona, AsyncDaytona # was: from daytona import ...
|
|
109
|
+
from withruntime.vercel import sandbox # was: from vercel import sandbox
|
|
110
|
+
from withruntime.vercel.sandbox import sync as sandbox # was: from vercel.sandbox import sync as sandbox
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Sandboxes get the rival's defaults: Daytona's 1 vCPU, 1 GiB and 3 GiB disk,
|
|
114
|
+
pausing after 15 minutes without calls; Vercel's 2 vCPUs with 2048 MiB each, 5
|
|
115
|
+
minutes, persistent. `runtime_create={...}` passes Runtime's own create
|
|
116
|
+
fields. A Daytona or Vercel key is never sent anywhere. What Runtime does not do
|
|
117
|
+
the same way raises `NotSupportedError` before anything happens, naming what to
|
|
118
|
+
use instead. The sync modules are generated from the async ones by
|
|
119
|
+
`scripts/generate_dropin_sync.py`. `DAYTONA.md` and `VERCEL.md` in the
|
|
120
|
+
JavaScript package list every mapping and gap.
|
|
121
|
+
|
|
122
|
+
## Agent frameworks
|
|
123
|
+
|
|
124
|
+
`withruntime.openai_agents` is a sandbox client for the OpenAI Agents SDK's
|
|
125
|
+
`SandboxAgent` (`pip install "withruntime[openai-agents]"`), and
|
|
126
|
+
`withruntime.deepagents` is a Deep Agents sandbox backend. `withruntime.tools`
|
|
127
|
+
gives LangChain, CrewAI, LlamaIndex, Pydantic AI, Google ADK and any framework
|
|
128
|
+
that takes typed functions four sandbox tools. See
|
|
129
|
+
https://withruntime.com/docs/frameworks.
|
|
130
|
+
|
|
131
|
+
Docs: https://withruntime.com/docs/python.
|
|
132
|
+
|
|
133
|
+
The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
|
|
134
|
+
0.3.0. Both names still work: `withruntime-cloud` installs this package and
|
|
135
|
+
`import runtime_cloud` gives you the same classes.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Runtime Cloud Python SDK
|
|
2
|
+
|
|
3
|
+
One client for every Runtime Cloud product, sync and async. Python 3.10 or
|
|
4
|
+
later, standard library only.
|
|
5
|
+
|
|
6
|
+
```bash no-run
|
|
7
|
+
pip install withruntime
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The client uses `RUNTIME_API_KEY` when it is set, and otherwise the connection
|
|
11
|
+
this machine saved when `npx withruntime login` connected it (one browser
|
|
12
|
+
approval, no key to copy). On a server or a CI runner, set `RUNTIME_API_KEY`
|
|
13
|
+
from your secret manager: `npx withruntime keys create` prints a new key once
|
|
14
|
+
after an owner, admin or developer of the account approves it in the browser, or create one at
|
|
15
|
+
https://withruntime.com/account/keys. Never put a key in source code, a URL or a
|
|
16
|
+
command-line argument.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from withruntime import Sandbox
|
|
20
|
+
|
|
21
|
+
with Sandbox.create() as sbx:
|
|
22
|
+
result = sbx.exec("python3 -c 'print(6 * 7)'")
|
|
23
|
+
print(result.exit_code, result.stdout)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`Sandbox.create()` needs no arguments and returns once the sandbox is running;
|
|
27
|
+
leaving the `with` block stops it. With no arguments you get the free trial
|
|
28
|
+
while it lasts: 50 free hours, no card, up to eight sandboxes running at once.
|
|
29
|
+
`AsyncRuntime` is the same client for asyncio, method for method:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
from withruntime import AsyncRuntime
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
async with AsyncRuntime() as runtime:
|
|
38
|
+
async with await runtime.sandboxes.create() as sbx:
|
|
39
|
+
print((await sbx.exec("uname -a")).stdout)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `forward_port` (any
|
|
46
|
+
TCP port in the sandbox on a local port), `files` (read, write,
|
|
47
|
+
list, glob, stat, move, remove, upload and download directories), `pause`,
|
|
48
|
+
`wake`, `extend`, `update`, `keep_alive`, `fork` and `snapshot`, and the
|
|
49
|
+
`interpreter`, `network`, `previews` and `desktop` products. A paused sandbox
|
|
50
|
+
also wakes by itself on the next call, and `Sandbox.get_or_create(name)` returns
|
|
51
|
+
the sandbox with that name or creates it. The client has `sandboxes`, `images`,
|
|
52
|
+
`volumes`, `snapshots`, `limits`, `feedback` and `support`;
|
|
53
|
+
`runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
|
|
54
|
+
what its agent may still spend today. Every write carries an
|
|
55
|
+
idempotency key, so retries never do anything twice; errors are typed and carry
|
|
56
|
+
`code`, `hint` and `request_id`.
|
|
57
|
+
|
|
58
|
+
## Behind a proxy
|
|
59
|
+
|
|
60
|
+
The client reads `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` (upper or lower
|
|
61
|
+
case; lower wins when both are set), sync and async. Calls to the API and
|
|
62
|
+
terminal WebSockets go through `HTTPS_PROXY` as a CONNECT tunnel, with any
|
|
63
|
+
`user:password@` in its address sent to the proxy. `HTTP_PROXY` is used only
|
|
64
|
+
for `http://` addresses, never for the HTTPS API. `NO_PROXY` lists hosts to
|
|
65
|
+
reach directly, split by commas or spaces: a name covers its subdomains, a
|
|
66
|
+
leading dot is allowed, `host:port` limits it to one port, and `*` means every
|
|
67
|
+
host. A proxy address without a scheme is `http://`; only `http://` proxies are
|
|
68
|
+
supported, and anything else fails at once with `invalid_proxy`. When a call
|
|
69
|
+
cannot get through, the error names the proxy it tried (without the password),
|
|
70
|
+
for example `No answer from Runtime at https://api.withruntime.com through the
|
|
71
|
+
proxy http://proxy.internal:3128 (HTTPS_PROXY).`
|
|
72
|
+
|
|
73
|
+
## Code written for E2B
|
|
74
|
+
|
|
75
|
+
`withruntime.e2b` runs code written for E2B's Python SDK on Runtime. Change
|
|
76
|
+
the import and set `RUNTIME_API_KEY`:
|
|
77
|
+
|
|
78
|
+
```python no-run
|
|
79
|
+
from withruntime.e2b import Sandbox, AsyncSandbox # was: from e2b import ...
|
|
80
|
+
from withruntime.e2b.code_interpreter import Sandbox # was: from e2b_code_interpreter import Sandbox
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Sandboxes get E2B's defaults: 2 vCPU, 512 MiB and a 300-second timeout.
|
|
84
|
+
Timeouts are in seconds, as in E2B's Python SDK. `runtime_create={...}` passes
|
|
85
|
+
Runtime's own create fields, for example `{"funding": "trial"}`. What Runtime
|
|
86
|
+
does not do the way E2B does raises `NotSupportedException` before anything
|
|
87
|
+
happens, naming what to use instead. Importing `withruntime` alone does not
|
|
88
|
+
load it. The sync `Sandbox` is generated from the async one by
|
|
89
|
+
`scripts/generate_e2b_sync.py`.
|
|
90
|
+
|
|
91
|
+
## Code written for Daytona or Vercel Sandbox
|
|
92
|
+
|
|
93
|
+
`withruntime.daytona` and `withruntime.vercel` do the same for Daytona's
|
|
94
|
+
Python SDK and Vercel Sandbox's. Change the import:
|
|
95
|
+
|
|
96
|
+
```python no-run
|
|
97
|
+
from withruntime.daytona import Daytona, AsyncDaytona # was: from daytona import ...
|
|
98
|
+
from withruntime.vercel import sandbox # was: from vercel import sandbox
|
|
99
|
+
from withruntime.vercel.sandbox import sync as sandbox # was: from vercel.sandbox import sync as sandbox
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Sandboxes get the rival's defaults: Daytona's 1 vCPU, 1 GiB and 3 GiB disk,
|
|
103
|
+
pausing after 15 minutes without calls; Vercel's 2 vCPUs with 2048 MiB each, 5
|
|
104
|
+
minutes, persistent. `runtime_create={...}` passes Runtime's own create
|
|
105
|
+
fields. A Daytona or Vercel key is never sent anywhere. What Runtime does not do
|
|
106
|
+
the same way raises `NotSupportedError` before anything happens, naming what to
|
|
107
|
+
use instead. The sync modules are generated from the async ones by
|
|
108
|
+
`scripts/generate_dropin_sync.py`. `DAYTONA.md` and `VERCEL.md` in the
|
|
109
|
+
JavaScript package list every mapping and gap.
|
|
110
|
+
|
|
111
|
+
## Agent frameworks
|
|
112
|
+
|
|
113
|
+
`withruntime.openai_agents` is a sandbox client for the OpenAI Agents SDK's
|
|
114
|
+
`SandboxAgent` (`pip install "withruntime[openai-agents]"`), and
|
|
115
|
+
`withruntime.deepagents` is a Deep Agents sandbox backend. `withruntime.tools`
|
|
116
|
+
gives LangChain, CrewAI, LlamaIndex, Pydantic AI, Google ADK and any framework
|
|
117
|
+
that takes typed functions four sandbox tools. See
|
|
118
|
+
https://withruntime.com/docs/frameworks.
|
|
119
|
+
|
|
120
|
+
Docs: https://withruntime.com/docs/python.
|
|
121
|
+
|
|
122
|
+
The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
|
|
123
|
+
0.3.0. Both names still work: `withruntime-cloud` installs this package and
|
|
124
|
+
`import runtime_cloud` gives you the same classes.
|
|
@@ -4,12 +4,18 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "withruntime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
dependencies = []
|
|
12
12
|
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
# withruntime.openai_agents: a sandbox client for the OpenAI Agents SDK's SandboxAgent.
|
|
15
|
+
openai-agents = ["openai-agents>=0.22.3"]
|
|
16
|
+
# withruntime.deepagents: a LangChain Deep Agents sandbox backend.
|
|
17
|
+
deepagents = ["deepagents>=0.7"]
|
|
18
|
+
|
|
13
19
|
[tool.setuptools.packages.find]
|
|
14
20
|
where = ["."]
|
|
15
21
|
include = ["withruntime*"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""runtime.audit.list() against a stub API, sync and async. The route and who
|
|
2
|
+
may read it are tested in packages/db (teams) and packages/cloud."""
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import threading
|
|
6
|
+
import unittest
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
|
|
9
|
+
from withruntime import AsyncRuntime, Runtime
|
|
10
|
+
|
|
11
|
+
PAGE = {
|
|
12
|
+
"events": [
|
|
13
|
+
{
|
|
14
|
+
"seq": "7",
|
|
15
|
+
"action": "key.created",
|
|
16
|
+
"actor": {"kind": "person", "id": "p", "name": "Marc", "person": None},
|
|
17
|
+
"ip": "203.0.113.7",
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"next": None,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Stub(BaseHTTPRequestHandler):
|
|
25
|
+
seen: list = []
|
|
26
|
+
|
|
27
|
+
def log_message(self, *args):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def do_GET(self):
|
|
31
|
+
Stub.seen.append((self.command, self.path))
|
|
32
|
+
data = json.dumps(PAGE).encode()
|
|
33
|
+
self.send_response(200)
|
|
34
|
+
self.send_header("content-type", "application/json")
|
|
35
|
+
self.send_header("content-length", str(len(data)))
|
|
36
|
+
self.end_headers()
|
|
37
|
+
self.wfile.write(data)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AuditTest(unittest.TestCase):
|
|
41
|
+
@classmethod
|
|
42
|
+
def setUpClass(cls):
|
|
43
|
+
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), Stub)
|
|
44
|
+
threading.Thread(target=cls.server.serve_forever, daemon=True).start()
|
|
45
|
+
cls.url = f"http://127.0.0.1:{cls.server.server_address[1]}"
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def tearDownClass(cls):
|
|
49
|
+
cls.server.shutdown()
|
|
50
|
+
|
|
51
|
+
def setUp(self):
|
|
52
|
+
Stub.seen = []
|
|
53
|
+
|
|
54
|
+
def test_sync_list_with_filters(self):
|
|
55
|
+
with Runtime(api_key="rk_test", base_url=self.url, max_retries=0) as runtime:
|
|
56
|
+
page = runtime.audit.list(action="member.", limit=10)
|
|
57
|
+
self.assertEqual(page["events"][0]["action"], "key.created")
|
|
58
|
+
self.assertEqual(Stub.seen, [("GET", "/v1/audit?action=member.&limit=10")])
|
|
59
|
+
|
|
60
|
+
def test_async_list(self):
|
|
61
|
+
async def go():
|
|
62
|
+
async with AsyncRuntime(api_key="rk_test", base_url=self.url, max_retries=0) as runtime:
|
|
63
|
+
return await runtime.audit.list()
|
|
64
|
+
|
|
65
|
+
self.assertIsNone(asyncio.run(go())["next"])
|
|
66
|
+
self.assertEqual(Stub.seen, [("GET", "/v1/audit")])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
unittest.main()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""A create that finds every trial slot, the account's quota or the region full
|
|
2
|
+
waits for room and sends the same call again, instead of failing a CI job that
|
|
3
|
+
burst past its limit (judge panel, 23 September 2026). Sync and async share
|
|
4
|
+
the transport's code, so both are driven here over a fake wire."""
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
from withruntime import AsyncRuntime, ConflictError, Runtime
|
|
11
|
+
|
|
12
|
+
RUNNING = json.dumps({"id": "sbx", "kind": "sandbox", "state": "running", "status": "active"}).encode()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def refusal(code, status=409, details=None):
|
|
16
|
+
error = {"code": code, "status": status, "message": "full", "retryAfterMs": 5}
|
|
17
|
+
if details is not None:
|
|
18
|
+
error["details"] = details
|
|
19
|
+
return status, json.dumps({"error": error}).encode()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Wire:
|
|
23
|
+
"""Answers each call with the next refusal, then with a running sandbox."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, refusals, is_async):
|
|
26
|
+
self.refusals, self.is_async = list(refusals), is_async
|
|
27
|
+
self.keys, self.bodies, self.targets = [], [], []
|
|
28
|
+
|
|
29
|
+
def _answer(self, method, target, headers, data):
|
|
30
|
+
self.keys.append(headers.get("Idempotency-Key"))
|
|
31
|
+
self.bodies.append(data)
|
|
32
|
+
self.targets.append(target)
|
|
33
|
+
status, body = self.refusals.pop(0) if self.refusals else (200, RUNNING)
|
|
34
|
+
is_async = self.is_async
|
|
35
|
+
|
|
36
|
+
class Answer:
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.status, self.headers = status, {}
|
|
39
|
+
|
|
40
|
+
if is_async:
|
|
41
|
+
async def read(self):
|
|
42
|
+
return body
|
|
43
|
+
else:
|
|
44
|
+
def read(self):
|
|
45
|
+
return body
|
|
46
|
+
return Answer()
|
|
47
|
+
|
|
48
|
+
def send(self, method, target, headers, data, timeout):
|
|
49
|
+
if self.is_async:
|
|
50
|
+
async def later():
|
|
51
|
+
return self._answer(method, target, headers, data)
|
|
52
|
+
return later()
|
|
53
|
+
return self._answer(method, target, headers, data)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create(refusals, client_options=None, **create_options):
|
|
57
|
+
"""Runs one create, sync and async, and returns each run's wire and outcome."""
|
|
58
|
+
runs = []
|
|
59
|
+
for is_async in (False, True):
|
|
60
|
+
wire = Wire(refusals, is_async)
|
|
61
|
+
options = {"api_key": "rk", "base_url": "https://api.example.test", **(client_options or {})}
|
|
62
|
+
runtime = AsyncRuntime(**options) if is_async else Runtime(**options)
|
|
63
|
+
runtime._t._http = wire
|
|
64
|
+
started = time.monotonic()
|
|
65
|
+
try:
|
|
66
|
+
if is_async:
|
|
67
|
+
outcome = asyncio.run(runtime.sandboxes.create(**create_options))
|
|
68
|
+
else:
|
|
69
|
+
outcome = runtime.sandboxes.create(**create_options)
|
|
70
|
+
except Exception as error: # noqa: BLE001 - the outcome under test
|
|
71
|
+
outcome = error
|
|
72
|
+
runs.append((wire, outcome, time.monotonic() - started))
|
|
73
|
+
return runs
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CapacityWait(unittest.TestCase):
|
|
77
|
+
def test_a_full_trial_is_waited_out_with_the_same_key_and_input(self):
|
|
78
|
+
busy = refusal("trial_busy", details={"sandboxIds": ["a"], "concurrent": 8})
|
|
79
|
+
for wire, outcome, _ in create([busy, busy], funding="trial", labels={"ci": "1"}):
|
|
80
|
+
self.assertEqual(outcome.id, "sbx")
|
|
81
|
+
self.assertEqual(len(wire.keys), 3)
|
|
82
|
+
self.assertEqual(len(set(wire.keys)), 1)
|
|
83
|
+
self.assertIsNotNone(wire.keys[0])
|
|
84
|
+
self.assertEqual(len(set(wire.bodies)), 1)
|
|
85
|
+
self.assertEqual(json.loads(wire.bodies[0]), {"funding": "trial", "labels": {"ci": "1"}})
|
|
86
|
+
|
|
87
|
+
def test_the_paid_limit_a_domain_limit_and_a_full_region_are_waited_out_too(self):
|
|
88
|
+
for code in ("quota_exceeded", "trial_domain_limit", "no_capacity"):
|
|
89
|
+
for wire, outcome, _ in create([refusal(code)]):
|
|
90
|
+
self.assertEqual(outcome.id, "sbx", code)
|
|
91
|
+
self.assertEqual(len(wire.keys), 2)
|
|
92
|
+
# trial_capacity is a 503: waited for as room, not spent from max_retries.
|
|
93
|
+
for wire, outcome, _ in create([refusal("trial_capacity", 503)] * 6, {"max_retries": 1}):
|
|
94
|
+
self.assertEqual(outcome.id, "sbx")
|
|
95
|
+
self.assertEqual(len(wire.keys), 7)
|
|
96
|
+
|
|
97
|
+
def test_when_the_wait_runs_out_the_original_refusal_is_raised(self):
|
|
98
|
+
for wire, outcome, took in create([refusal("trial_busy")] * 1000, {"wait_for_capacity": 0.2}):
|
|
99
|
+
self.assertIsInstance(outcome, ConflictError)
|
|
100
|
+
self.assertEqual(outcome.code, "trial_busy")
|
|
101
|
+
self.assertGreater(len(wire.keys), 2)
|
|
102
|
+
self.assertGreaterEqual(took, 0.19)
|
|
103
|
+
self.assertLess(took, 1.5)
|
|
104
|
+
|
|
105
|
+
def test_zero_fails_at_once_on_the_client_or_on_one_call(self):
|
|
106
|
+
for wire, outcome, _ in create([refusal("trial_busy")], {"wait_for_capacity": 0}):
|
|
107
|
+
self.assertEqual(getattr(outcome, "code", None), "trial_busy")
|
|
108
|
+
self.assertEqual(len(wire.keys), 1)
|
|
109
|
+
for wire, outcome, _ in create([refusal("quota_exceeded")], wait_for_capacity=0):
|
|
110
|
+
self.assertEqual(getattr(outcome, "code", None), "quota_exceeded")
|
|
111
|
+
self.assertEqual(len(wire.keys), 1)
|
|
112
|
+
|
|
113
|
+
def test_a_request_that_can_never_fit_is_not_waited_for(self):
|
|
114
|
+
never = refusal("trial_busy", details={"field": "count", "concurrent": 8})
|
|
115
|
+
for wire, outcome, _ in create([never]):
|
|
116
|
+
self.assertEqual(getattr(outcome, "code", None), "trial_busy")
|
|
117
|
+
self.assertEqual(len(wire.keys), 1)
|
|
118
|
+
|
|
119
|
+
def test_only_a_create_waits(self):
|
|
120
|
+
# A fork's failure names copies that started, and its key replays that
|
|
121
|
+
# failure, so a fork is never retried by waiting.
|
|
122
|
+
wire = Wire([refusal("trial_busy")], False)
|
|
123
|
+
runtime = Runtime(api_key="rk", base_url="https://api.example.test")
|
|
124
|
+
runtime._t._http = wire
|
|
125
|
+
from withruntime._sync_client import Sandbox
|
|
126
|
+
sandbox = Sandbox(runtime._t, {"id": "sbx", "state": "running"})
|
|
127
|
+
with self.assertRaises(ConflictError):
|
|
128
|
+
sandbox.fork()
|
|
129
|
+
self.assertEqual(len(wire.keys), 1)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
unittest.main()
|
|
@@ -40,6 +40,15 @@ class Sync(unittest.TestCase):
|
|
|
40
40
|
piped = sbx.exec(["cat"], stdin="in", env={"TOKEN": "secret"})
|
|
41
41
|
self.assertIn("<in>", piped.stdout)
|
|
42
42
|
|
|
43
|
+
def test_fork_takes_funding_as_create_does(self):
|
|
44
|
+
# 23 September 2026: fork(funding="trial") was refused as "body.funding is
|
|
45
|
+
# not a known field". Forks are switched off on the fixture, so the real
|
|
46
|
+
# router's answer is the deliberate fork_unavailable, not a 400.
|
|
47
|
+
sbx = client().sandboxes.get(ID)
|
|
48
|
+
with self.assertRaises(ServiceUnavailableError) as caught:
|
|
49
|
+
sbx.fork(funding="trial")
|
|
50
|
+
self.assertEqual(caught.exception.code, "fork_unavailable")
|
|
51
|
+
|
|
43
52
|
def test_check_raises_with_the_output(self):
|
|
44
53
|
sbx = client().sandboxes.get(ID)
|
|
45
54
|
with self.assertRaises(CommandError) as caught:
|
|
@@ -340,5 +349,56 @@ class Routes(unittest.TestCase):
|
|
|
340
349
|
self.assertTrue(all(r.split(" ")[1].startswith("/v1/") for r in routes))
|
|
341
350
|
|
|
342
351
|
|
|
352
|
+
class Retryable(unittest.TestCase):
|
|
353
|
+
def test_refusals_that_clear_by_themselves_are_retryable(self):
|
|
354
|
+
from withruntime import ConflictError
|
|
355
|
+
self.assertTrue(ConflictError("x", code="trial_busy", status=409, details={"sandboxIds": []}).retryable)
|
|
356
|
+
self.assertTrue(ConflictError("x", code="no_capacity", status=409).retryable)
|
|
357
|
+
self.assertFalse(ConflictError("x", code="trial_busy", status=409, details={"field": "count"}).retryable)
|
|
358
|
+
self.assertFalse(ConflictError("x", code="sandbox_stopped", status=409).retryable)
|
|
359
|
+
self.assertFalse(ServiceUnavailableError("x", code="fork_unavailable", status=503).retryable)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class StreamedTruncation(unittest.TestCase):
|
|
363
|
+
"""A streamed result says whether output was lost: False unless the server
|
|
364
|
+
sent a "truncated" event, which it does when a reader falls behind the
|
|
365
|
+
process's output buffer."""
|
|
366
|
+
|
|
367
|
+
START = {"type": "start", "processId": "p1"}
|
|
368
|
+
OUT = {"type": "stdout", "data": "hello\n", "offset": 0}
|
|
369
|
+
DROP = {"type": "truncated", "droppedBytes": 10, "resumeAt": 10}
|
|
370
|
+
EXIT = {"type": "exit", "exitCode": 0, "state": "exited", "timedOut": False}
|
|
371
|
+
|
|
372
|
+
def flags(self, result):
|
|
373
|
+
return (result.stdout_truncated, result.stderr_truncated)
|
|
374
|
+
|
|
375
|
+
def test_sync(self):
|
|
376
|
+
for events, expected in (([self.START, self.OUT, self.EXIT], (False, False)),
|
|
377
|
+
([self.START, self.DROP, self.OUT, self.EXIT], (True, True))):
|
|
378
|
+
sbx = withruntime._Sandbox(None, {"id": ID})
|
|
379
|
+
sbx.exec_stream = lambda *_, _events=events, **__: iter(_events)
|
|
380
|
+
self.assertEqual(self.flags(sbx.exec("long job", on_stdout=lambda _: None)), expected)
|
|
381
|
+
proc = withruntime.Process(None, ID, {"id": "p1"})
|
|
382
|
+
proc.output = lambda *_, _events=events[1:], **__: iter(_events)
|
|
383
|
+
self.assertEqual(self.flags(proc.wait()), expected)
|
|
384
|
+
|
|
385
|
+
def test_async(self):
|
|
386
|
+
async def replay(events):
|
|
387
|
+
for event in events:
|
|
388
|
+
yield event
|
|
389
|
+
|
|
390
|
+
async def main():
|
|
391
|
+
for events, expected in (([self.START, self.OUT, self.EXIT], (False, False)),
|
|
392
|
+
([self.START, self.DROP, self.OUT, self.EXIT], (True, True))):
|
|
393
|
+
sbx = withruntime.AsyncSandbox(None, {"id": ID})
|
|
394
|
+
sbx.exec_stream = lambda *_, _events=events, **__: replay(_events)
|
|
395
|
+
self.assertEqual(self.flags(await sbx.exec("long job", on_stdout=lambda _: None)), expected)
|
|
396
|
+
proc = withruntime.AsyncProcess(None, ID, {"id": "p1"})
|
|
397
|
+
proc.output = lambda *_, _events=events[1:], **__: replay(_events)
|
|
398
|
+
self.assertEqual(self.flags(await proc.wait()), expected)
|
|
399
|
+
|
|
400
|
+
asyncio.run(main())
|
|
401
|
+
|
|
402
|
+
|
|
343
403
|
if __name__ == "__main__":
|
|
344
404
|
unittest.main()
|