withruntime 0.3.0__tar.gz → 0.4.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.
Files changed (56) hide show
  1. {withruntime-0.3.0 → withruntime-0.4.0}/PKG-INFO +37 -2
  2. withruntime-0.4.0/README.md +89 -0
  3. {withruntime-0.3.0 → withruntime-0.4.0}/pyproject.toml +1 -1
  4. {withruntime-0.3.0 → withruntime-0.4.0}/tests/test_client.py +9 -0
  5. withruntime-0.4.0/tests/test_e2b_code_interpreter.py +130 -0
  6. withruntime-0.4.0/tests/test_e2b_sandbox.py +413 -0
  7. withruntime-0.4.0/tests/test_limits.py +61 -0
  8. withruntime-0.4.0/tests/test_proxy.py +267 -0
  9. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_client.py +11 -6
  10. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/__init__.py +2 -0
  11. withruntime-0.4.0/withruntime/_async_products/limits.py +20 -0
  12. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_http.py +28 -7
  13. withruntime-0.4.0/withruntime/_proxy.py +177 -0
  14. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_client.py +11 -6
  15. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/__init__.py +2 -0
  16. withruntime-0.4.0/withruntime/_sync_products/limits.py +21 -0
  17. withruntime-0.4.0/withruntime/_version.py +1 -0
  18. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_ws.py +6 -5
  19. withruntime-0.4.0/withruntime/e2b/__init__.py +57 -0
  20. withruntime-0.4.0/withruntime/e2b/_async_io.py +54 -0
  21. withruntime-0.4.0/withruntime/e2b/_async_sandbox.py +641 -0
  22. withruntime-0.4.0/withruntime/e2b/_core.py +512 -0
  23. withruntime-0.4.0/withruntime/e2b/_sync_io.py +41 -0
  24. withruntime-0.4.0/withruntime/e2b/_sync_sandbox.py +640 -0
  25. withruntime-0.4.0/withruntime/e2b/code_interpreter/__init__.py +14 -0
  26. withruntime-0.4.0/withruntime/e2b/code_interpreter/_async_ci.py +143 -0
  27. withruntime-0.4.0/withruntime/e2b/code_interpreter/_models.py +101 -0
  28. withruntime-0.4.0/withruntime/e2b/code_interpreter/_sync_ci.py +142 -0
  29. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime.egg-info/PKG-INFO +37 -2
  30. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime.egg-info/SOURCES.txt +18 -1
  31. withruntime-0.3.0/README.md +0 -54
  32. withruntime-0.3.0/withruntime/_version.py +0 -1
  33. {withruntime-0.3.0 → withruntime-0.4.0}/setup.cfg +0 -0
  34. {withruntime-0.3.0 → withruntime-0.4.0}/tests/test_images_volumes_interpreter.py +0 -0
  35. {withruntime-0.3.0 → withruntime-0.4.0}/tests/test_previews_network_desktop.py +0 -0
  36. {withruntime-0.3.0 → withruntime-0.4.0}/tests/test_referrals.py +0 -0
  37. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/__init__.py +0 -0
  38. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/desktop.py +0 -0
  39. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/images.py +0 -0
  40. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/interpreter.py +0 -0
  41. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/network.py +0 -0
  42. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/previews.py +0 -0
  43. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/referrals.py +0 -0
  44. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_async_products/volumes.py +0 -0
  45. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_clock.py +0 -0
  46. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_connection.py +0 -0
  47. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_errors.py +0 -0
  48. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/desktop.py +0 -0
  49. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/images.py +0 -0
  50. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/interpreter.py +0 -0
  51. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/network.py +0 -0
  52. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/previews.py +0 -0
  53. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/referrals.py +0 -0
  54. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime/_sync_products/volumes.py +0 -0
  55. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime.egg-info/dependency_links.txt +0 -0
  56. {withruntime-0.3.0 → withruntime-0.4.0}/withruntime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: withruntime
3
- Version: 0.3.0
3
+ Version: 0.4.0
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
@@ -50,10 +50,45 @@ A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `files` (read, write,
50
50
  list, glob, stat, move, remove, upload and download directories), `pause`,
51
51
  `wake`, `extend`, `fork` and `snapshot`, and the `interpreter`, `network`,
52
52
  `previews` and `desktop` products. The client has `sandboxes`, `images`,
53
- `volumes`, `snapshots`, `feedback` and `support`. Every write carries an
53
+ `volumes`, `snapshots`, `limits`, `feedback` and `support`;
54
+ `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
55
+ what its agent may still spend today. Every write carries an
54
56
  idempotency key, so retries never do anything twice; errors are typed and carry
55
57
  `code`, `hint` and `request_id`.
56
58
 
59
+ ## Behind a proxy
60
+
61
+ The client reads `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` (upper or lower
62
+ case; lower wins when both are set), sync and async. Calls to the API and
63
+ terminal WebSockets go through `HTTPS_PROXY` as a CONNECT tunnel, with any
64
+ `user:password@` in its address sent to the proxy. `HTTP_PROXY` is used only
65
+ for `http://` addresses, never for the HTTPS API. `NO_PROXY` lists hosts to
66
+ reach directly, split by commas or spaces: a name covers its subdomains, a
67
+ leading dot is allowed, `host:port` limits it to one port, and `*` means every
68
+ host. A proxy address without a scheme is `http://`; only `http://` proxies are
69
+ supported, and anything else fails at once with `invalid_proxy`. When a call
70
+ cannot get through, the error names the proxy it tried (without the password),
71
+ for example `No answer from Runtime at https://api.withruntime.com through the
72
+ proxy http://proxy.internal:3128 (HTTPS_PROXY).`
73
+
74
+ ## Code written for E2B
75
+
76
+ `withruntime.e2b` runs code written for E2B's Python SDK on Runtime. Change
77
+ the import and set `RUNTIME_API_KEY`:
78
+
79
+ ```python no-run
80
+ from withruntime.e2b import Sandbox, AsyncSandbox # was: from e2b import ...
81
+ from withruntime.e2b.code_interpreter import Sandbox # was: from e2b_code_interpreter import Sandbox
82
+ ```
83
+
84
+ Sandboxes get E2B's defaults: 2 vCPU, 512 MiB and a 300-second timeout.
85
+ Timeouts are in seconds, as in E2B's Python SDK. `runtime_create={...}` passes
86
+ Runtime's own create fields, for example `{"funding": "trial"}`. What Runtime
87
+ does not do the way E2B does raises `NotSupportedException` before anything
88
+ happens, naming what to use instead. Importing `withruntime` alone does not
89
+ load it. The sync `Sandbox` is generated from the async one by
90
+ `scripts/generate_e2b_sync.py`.
91
+
57
92
  Docs: https://withruntime.com/docs/python.
58
93
 
59
94
  The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
@@ -0,0 +1,89 @@
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, set `RUNTIME_API_KEY` from your secret
13
+ manager (create a key at https://withruntime.com/account/keys). Never put a key
14
+ in source code, a URL or a command-line argument.
15
+
16
+ ```python
17
+ from withruntime import Sandbox
18
+
19
+ with Sandbox.create() as sbx:
20
+ result = sbx.exec("python3 -c 'print(6 * 7)'")
21
+ print(result.exit_code, result.stdout)
22
+ ```
23
+
24
+ `Sandbox.create()` needs no arguments and returns once the sandbox is running;
25
+ leaving the `with` block stops it. `AsyncRuntime` is the same client for
26
+ asyncio, method for method:
27
+
28
+ ```python
29
+ import asyncio
30
+ from withruntime import AsyncRuntime
31
+
32
+
33
+ async def main():
34
+ async with AsyncRuntime() as runtime:
35
+ async with await runtime.sandboxes.create() as sbx:
36
+ print((await sbx.exec("uname -a")).stdout)
37
+
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ A sandbox has `exec`, `exec_stream`, `spawn`, `terminal`, `files` (read, write,
43
+ list, glob, stat, move, remove, upload and download directories), `pause`,
44
+ `wake`, `extend`, `fork` and `snapshot`, and the `interpreter`, `network`,
45
+ `previews` and `desktop` products. The client has `sandboxes`, `images`,
46
+ `volumes`, `snapshots`, `limits`, `feedback` and `support`;
47
+ `runtime.limits.get()` (0.3.1 and later) says whether the key is read-only and
48
+ what its agent may still spend today. Every write carries an
49
+ idempotency key, so retries never do anything twice; errors are typed and carry
50
+ `code`, `hint` and `request_id`.
51
+
52
+ ## Behind a proxy
53
+
54
+ The client reads `HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY` (upper or lower
55
+ case; lower wins when both are set), sync and async. Calls to the API and
56
+ terminal WebSockets go through `HTTPS_PROXY` as a CONNECT tunnel, with any
57
+ `user:password@` in its address sent to the proxy. `HTTP_PROXY` is used only
58
+ for `http://` addresses, never for the HTTPS API. `NO_PROXY` lists hosts to
59
+ reach directly, split by commas or spaces: a name covers its subdomains, a
60
+ leading dot is allowed, `host:port` limits it to one port, and `*` means every
61
+ host. A proxy address without a scheme is `http://`; only `http://` proxies are
62
+ supported, and anything else fails at once with `invalid_proxy`. When a call
63
+ cannot get through, the error names the proxy it tried (without the password),
64
+ for example `No answer from Runtime at https://api.withruntime.com through the
65
+ proxy http://proxy.internal:3128 (HTTPS_PROXY).`
66
+
67
+ ## Code written for E2B
68
+
69
+ `withruntime.e2b` runs code written for E2B's Python SDK on Runtime. Change
70
+ the import and set `RUNTIME_API_KEY`:
71
+
72
+ ```python no-run
73
+ from withruntime.e2b import Sandbox, AsyncSandbox # was: from e2b import ...
74
+ from withruntime.e2b.code_interpreter import Sandbox # was: from e2b_code_interpreter import Sandbox
75
+ ```
76
+
77
+ Sandboxes get E2B's defaults: 2 vCPU, 512 MiB and a 300-second timeout.
78
+ Timeouts are in seconds, as in E2B's Python SDK. `runtime_create={...}` passes
79
+ Runtime's own create fields, for example `{"funding": "trial"}`. What Runtime
80
+ does not do the way E2B does raises `NotSupportedException` before anything
81
+ happens, naming what to use instead. Importing `withruntime` alone does not
82
+ load it. The sync `Sandbox` is generated from the async one by
83
+ `scripts/generate_e2b_sync.py`.
84
+
85
+ Docs: https://withruntime.com/docs/python.
86
+
87
+ The package was called `withruntime-cloud`, imported as `runtime_cloud`, until
88
+ 0.3.0. Both names still work: `withruntime-cloud` installs this package and
89
+ `import runtime_cloud` gives you the same classes.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "withruntime"
7
- version = "0.3.0"
7
+ version = "0.4.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"
@@ -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:
@@ -0,0 +1,130 @@
1
+ """The code interpreter adapter against the fake withruntime SDK."""
2
+ import asyncio
3
+ import sys
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
8
+
9
+ from e2b_fake import World # noqa: E402
10
+
11
+ from withruntime.e2b.code_interpreter import (AsyncSandbox, CommandExitException, Context, Execution, # noqa: E402
12
+ NotSupportedException, OutputMessage, Result, Sandbox,
13
+ TimeoutException)
14
+
15
+
16
+ class RunCode(unittest.TestCase):
17
+ def setUp(self) -> None:
18
+ self.world = World()
19
+
20
+ def create(self, **kwargs):
21
+ return Sandbox.create(client=self.world.client(), **kwargs)
22
+
23
+ def test_python_by_default(self):
24
+ sbx = self.create()
25
+ execution = sbx.run_code("1 + 1")
26
+ self.assertIsInstance(execution, Execution)
27
+ self.assertEqual(execution.text, "2")
28
+ self.assertIsInstance(execution.results[0], Result)
29
+ self.assertEqual(execution.results[0].formats(), ["text"])
30
+ self.assertEqual((execution.logs.stdout, execution.logs.stderr), (["out 1 + 1\n"], []))
31
+ self.assertEqual(execution.execution_count, 1)
32
+ self.assertIsNone(execution.error)
33
+ self.assertEqual(self.world.called("interpreter.run")[0],
34
+ ("1 + 1", {"language": "python", "timeout_ms": 60_000}))
35
+
36
+ def test_base_commands_still_work(self):
37
+ from e2b_fake import Result as Run
38
+ self.world.exec = lambda *_: Run(1, "", "no\n")
39
+ with self.assertRaises(CommandExitException):
40
+ self.create().commands.run("false")
41
+
42
+ def test_rich_results(self):
43
+ self.world.interpreter = lambda *_: {
44
+ "status": "ok", "stdout": "a\nb\npartial", "stderr": "warn\n", "executionCount": 4, "error": None,
45
+ "results": [
46
+ {"main": False, "data": {"image/png": "iVBOR", "text/plain": "<Figure>"}, "refs": {}},
47
+ {"main": True, "data": {"application/vnd.runtime.table+json": {"columns": ["a"]}},
48
+ "refs": {"text/html": {"path": "/workspace/.runtime/interpreter/python/out/1.html"}}}]}
49
+ execution = self.create().run_code("df")
50
+ self.assertEqual(execution.logs.stdout, ["a\n", "b\n", "partial"])
51
+ figure, table = execution.results
52
+ self.assertEqual((figure.png, figure.text, figure.is_main_result), ("iVBOR", "<Figure>", False))
53
+ self.assertEqual(table.html, "bytes of /workspace/.runtime/interpreter/python/out/1.html")
54
+ self.assertEqual(table.extra, {"application/vnd.runtime.table+json": {"columns": ["a"]}})
55
+ self.assertIsNone(table.data)
56
+ self.assertIsNone(table.chart)
57
+
58
+ def test_errors_timeouts_and_refusals(self):
59
+ self.world.interpreter = lambda *_: {"status": "error", "stdout": "", "stderr": "", "results": [],
60
+ "error": {"name": "ZeroDivisionError", "value": "division by zero",
61
+ "traceback": "..."}}
62
+ sbx = self.create()
63
+ self.assertEqual(sbx.run_code("1/0").error.name, "ZeroDivisionError")
64
+ self.world.interpreter = lambda *_: {"status": "timeout", "stdout": "", "stderr": "", "results": []}
65
+ with self.assertRaises(TimeoutException):
66
+ sbx.run_code("while True: pass", timeout=1)
67
+ for language in ("bash", "r", "typescript", "java"):
68
+ with self.assertRaises(NotSupportedException):
69
+ sbx.run_code("1", language=language)
70
+ with self.assertRaises(NotSupportedException):
71
+ sbx.run_code("1", envs={"A": "1"})
72
+
73
+ def test_streams(self):
74
+ sbx = self.create()
75
+ out, results = [], []
76
+ sbx.run_code("print(1)", on_stdout=out.append, on_result=results.append)
77
+ self.assertIsInstance(out[0], OutputMessage)
78
+ self.assertEqual((out[0].line, out[0].error), ("out print(1)\n", False))
79
+ self.assertEqual([one.text for one in results], ["2"])
80
+
81
+ def test_sandbox_envs_reach_code_through_one_context(self):
82
+ sbx = self.create(envs={"TOKEN": "t"})
83
+ sbx.run_code("import os")
84
+ sbx.run_code("os.environ['TOKEN']")
85
+ self.assertEqual(self.world.called("contexts.create"),
86
+ [({"id": "e2b-python", "language": "python", "env": {"TOKEN": "t"}},)])
87
+ self.assertEqual([call[1] for call in self.world.called("interpreter.run")],
88
+ [{"context": "e2b-python", "timeout_ms": 60_000}] * 2)
89
+ other = Sandbox.create(client=self.world.client(), envs={"TOKEN": "t"})
90
+ other.runtime.contexts["e2b-python"] = {"id": "e2b-python", "language": "python", "cwd": "/workspace"}
91
+ other.run_code("1")
92
+ self.assertEqual(self.world.called("interpreter.run")[-1][1]["context"], "e2b-python")
93
+
94
+ def test_contexts(self):
95
+ sbx = self.create()
96
+ context = sbx.create_code_context(cwd="/tmp", language="javascript")
97
+ self.assertEqual(context, Context("ctx-1", "javascript", "/tmp"))
98
+ self.assertEqual(sbx.list_code_contexts(), [context])
99
+ sbx.run_code("x", context=context)
100
+ self.assertEqual(self.world.called("interpreter.run")[0][1], {"context": "ctx-1", "timeout_ms": 60_000})
101
+ sbx.restart_code_context(context)
102
+ sbx.remove_code_context("ctx-1")
103
+ self.assertEqual(self.world.called("contexts.remove"), [("ctx-1",)])
104
+ with self.assertRaises(NotSupportedException):
105
+ sbx.create_code_context(language="r")
106
+
107
+
108
+ class AsyncRunCode(unittest.TestCase):
109
+ def test_async(self):
110
+ world = World()
111
+
112
+ async def scenario():
113
+ seen = []
114
+
115
+ async def on_stdout(message):
116
+ seen.append(message.line)
117
+
118
+ sbx = await AsyncSandbox.create(client=world.async_client())
119
+ execution = await sbx.run_code("1 + 1", on_stdout=on_stdout)
120
+ await asyncio.sleep(0)
121
+ self.assertEqual(execution.text, "2")
122
+ self.assertEqual(seen, ["out 1 + 1\n"])
123
+ context = await sbx.create_code_context(language="python")
124
+ self.assertEqual((await sbx.list_code_contexts())[0].id, context.id)
125
+
126
+ asyncio.run(scenario())
127
+
128
+
129
+ if __name__ == "__main__":
130
+ unittest.main()