withruntime 0.4.0__tar.gz → 0.5.1__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.4.0/withruntime.egg-info → withruntime-0.5.1}/PKG-INFO +48 -9
- withruntime-0.4.0/PKG-INFO → withruntime-0.5.1/README.md +43 -15
- {withruntime-0.4.0 → withruntime-0.5.1}/pyproject.toml +7 -1
- withruntime-0.5.1/tests/test_audit.py +70 -0
- withruntime-0.5.1/tests/test_capacity_wait.py +133 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_client.py +51 -0
- withruntime-0.5.1/tests/test_daytona.py +373 -0
- withruntime-0.5.1/tests/test_deepagents.py +79 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_e2b_sandbox.py +13 -4
- withruntime-0.5.1/tests/test_images_v2.py +137 -0
- withruntime-0.5.1/tests/test_lifecycle_settings.py +135 -0
- withruntime-0.5.1/tests/test_observability.py +134 -0
- withruntime-0.5.1/tests/test_openai_agents.py +456 -0
- withruntime-0.5.1/tests/test_secrets.py +91 -0
- withruntime-0.5.1/tests/test_switching.py +79 -0
- withruntime-0.5.1/tests/test_tools.py +115 -0
- withruntime-0.5.1/tests/test_vercel.py +282 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/__init__.py +10 -1
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_client.py +149 -13
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/__init__.py +11 -0
- withruntime-0.5.1/withruntime/_async_products/audit.py +26 -0
- withruntime-0.5.1/withruntime/_async_products/images.py +379 -0
- withruntime-0.5.1/withruntime/_async_products/observability.py +128 -0
- withruntime-0.5.1/withruntime/_async_products/secrets.py +38 -0
- withruntime-0.5.1/withruntime/_async_products/switching.py +36 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_clock.py +15 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_errors.py +21 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_client.py +149 -13
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/__init__.py +11 -0
- withruntime-0.5.1/withruntime/_sync_products/audit.py +27 -0
- withruntime-0.5.1/withruntime/_sync_products/images.py +380 -0
- withruntime-0.5.1/withruntime/_sync_products/observability.py +129 -0
- withruntime-0.5.1/withruntime/_sync_products/secrets.py +39 -0
- withruntime-0.5.1/withruntime/_sync_products/switching.py +37 -0
- withruntime-0.5.1/withruntime/_tunnel.py +330 -0
- withruntime-0.5.1/withruntime/_version.py +1 -0
- withruntime-0.5.1/withruntime/daytona/__init__.py +74 -0
- withruntime-0.5.1/withruntime/daytona/_async_daytona.py +1031 -0
- withruntime-0.5.1/withruntime/daytona/_async_io.py +34 -0
- withruntime-0.5.1/withruntime/daytona/_core.py +924 -0
- withruntime-0.5.1/withruntime/daytona/_sync_daytona.py +1029 -0
- withruntime-0.5.1/withruntime/daytona/_sync_io.py +43 -0
- withruntime-0.5.1/withruntime/deepagents.py +131 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/_async_sandbox.py +45 -4
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/_core.py +8 -4
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/_sync_sandbox.py +45 -4
- withruntime-0.5.1/withruntime/openai_agents.py +844 -0
- withruntime-0.5.1/withruntime/tools.py +201 -0
- withruntime-0.5.1/withruntime/vercel/__init__.py +12 -0
- withruntime-0.5.1/withruntime/vercel/_async_io.py +58 -0
- withruntime-0.5.1/withruntime/vercel/_async_sandbox.py +862 -0
- withruntime-0.5.1/withruntime/vercel/_core.py +438 -0
- withruntime-0.5.1/withruntime/vercel/_sync_io.py +51 -0
- withruntime-0.5.1/withruntime/vercel/_sync_sandbox.py +860 -0
- withruntime-0.5.1/withruntime/vercel/api.py +32 -0
- withruntime-0.5.1/withruntime/vercel/sandbox/__init__.py +56 -0
- withruntime-0.5.1/withruntime/vercel/sandbox/sync.py +26 -0
- withruntime-0.5.1/withruntime/webhooks.py +56 -0
- withruntime-0.4.0/README.md → withruntime-0.5.1/withruntime.egg-info/PKG-INFO +54 -8
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime.egg-info/SOURCES.txt +42 -1
- withruntime-0.5.1/withruntime.egg-info/requires.txt +6 -0
- withruntime-0.4.0/withruntime/_async_products/images.py +0 -119
- withruntime-0.4.0/withruntime/_sync_products/images.py +0 -120
- withruntime-0.4.0/withruntime/_version.py +0 -1
- {withruntime-0.4.0 → withruntime-0.5.1}/setup.cfg +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_e2b_code_interpreter.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_images_volumes_interpreter.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_limits.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_previews_network_desktop.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_proxy.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/tests/test_referrals.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/desktop.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/interpreter.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/limits.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/network.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/previews.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/referrals.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_async_products/volumes.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_connection.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_http.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_proxy.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/desktop.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/interpreter.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/limits.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/network.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/previews.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/referrals.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_sync_products/volumes.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/_ws.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/__init__.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/_async_io.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/_sync_io.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/code_interpreter/__init__.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/code_interpreter/_async_ci.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/code_interpreter/_models.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime/e2b/code_interpreter/_sync_ci.py +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime.egg-info/dependency_links.txt +0 -0
- {withruntime-0.4.0 → withruntime-0.5.1}/withruntime.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withruntime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Runtime Cloud: one client (sync and async) for every Runtime product. Sandboxes first.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
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"
|
|
7
11
|
|
|
8
12
|
# Runtime Cloud Python SDK
|
|
9
13
|
|
|
@@ -16,9 +20,11 @@ pip install withruntime
|
|
|
16
20
|
|
|
17
21
|
The client uses `RUNTIME_API_KEY` when it is set, and otherwise the connection
|
|
18
22
|
this machine saved when `npx withruntime login` connected it (one browser
|
|
19
|
-
approval, no key to copy). On a server, set `RUNTIME_API_KEY`
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
22
28
|
|
|
23
29
|
```python
|
|
24
30
|
from withruntime import Sandbox
|
|
@@ -29,8 +35,9 @@ with Sandbox.create() as sbx:
|
|
|
29
35
|
```
|
|
30
36
|
|
|
31
37
|
`Sandbox.create()` needs no arguments and returns once the sandbox is running;
|
|
32
|
-
leaving the `with` block stops it.
|
|
33
|
-
|
|
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:
|
|
34
41
|
|
|
35
42
|
```python
|
|
36
43
|
import asyncio
|
|
@@ -46,10 +53,13 @@ async def main():
|
|
|
46
53
|
asyncio.run(main())
|
|
47
54
|
```
|
|
48
55
|
|
|
49
|
-
A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `
|
|
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,
|
|
50
58
|
list, glob, stat, move, remove, upload and download directories), `pause`,
|
|
51
|
-
`wake`, `extend`, `fork` and `snapshot`, and the
|
|
52
|
-
`previews` and `desktop` products.
|
|
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`,
|
|
53
63
|
`volumes`, `snapshots`, `limits`, `feedback` and `support`;
|
|
54
64
|
`runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
|
|
55
65
|
what its agent may still spend today. Every write carries an
|
|
@@ -89,6 +99,35 @@ happens, naming what to use instead. Importing `withruntime` alone does not
|
|
|
89
99
|
load it. The sync `Sandbox` is generated from the async one by
|
|
90
100
|
`scripts/generate_e2b_sync.py`.
|
|
91
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
|
+
|
|
92
131
|
Docs: https://withruntime.com/docs/python.
|
|
93
132
|
|
|
94
133
|
The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: withruntime
|
|
3
|
-
Version: 0.4.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
|
-
|
|
8
1
|
# Runtime Cloud Python SDK
|
|
9
2
|
|
|
10
3
|
One client for every Runtime Cloud product, sync and async. Python 3.10 or
|
|
@@ -16,9 +9,11 @@ pip install withruntime
|
|
|
16
9
|
|
|
17
10
|
The client uses `RUNTIME_API_KEY` when it is set, and otherwise the connection
|
|
18
11
|
this machine saved when `npx withruntime login` connected it (one browser
|
|
19
|
-
approval, no key to copy). On a server, set `RUNTIME_API_KEY`
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
22
17
|
|
|
23
18
|
```python
|
|
24
19
|
from withruntime import Sandbox
|
|
@@ -29,8 +24,9 @@ with Sandbox.create() as sbx:
|
|
|
29
24
|
```
|
|
30
25
|
|
|
31
26
|
`Sandbox.create()` needs no arguments and returns once the sandbox is running;
|
|
32
|
-
leaving the `with` block stops it.
|
|
33
|
-
|
|
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:
|
|
34
30
|
|
|
35
31
|
```python
|
|
36
32
|
import asyncio
|
|
@@ -46,10 +42,13 @@ async def main():
|
|
|
46
42
|
asyncio.run(main())
|
|
47
43
|
```
|
|
48
44
|
|
|
49
|
-
A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `
|
|
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,
|
|
50
47
|
list, glob, stat, move, remove, upload and download directories), `pause`,
|
|
51
|
-
`wake`, `extend`, `fork` and `snapshot`, and the
|
|
52
|
-
`previews` and `desktop` products.
|
|
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`,
|
|
53
52
|
`volumes`, `snapshots`, `limits`, `feedback` and `support`;
|
|
54
53
|
`runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
|
|
55
54
|
what its agent may still spend today. Every write carries an
|
|
@@ -89,6 +88,35 @@ happens, naming what to use instead. Importing `withruntime` alone does not
|
|
|
89
88
|
load it. The sync `Sandbox` is generated from the async one by
|
|
90
89
|
`scripts/generate_e2b_sync.py`.
|
|
91
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
|
+
|
|
92
120
|
Docs: https://withruntime.com/docs/python.
|
|
93
121
|
|
|
94
122
|
The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
|
|
@@ -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.1"
|
|
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()
|
|
@@ -349,5 +349,56 @@ class Routes(unittest.TestCase):
|
|
|
349
349
|
self.assertTrue(all(r.split(" ")[1].startswith("/v1/") for r in routes))
|
|
350
350
|
|
|
351
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
|
+
|
|
352
403
|
if __name__ == "__main__":
|
|
353
404
|
unittest.main()
|