solari-e2b-compat 0.1.0__tar.gz → 0.2.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.
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/COMPAT.md +1 -1
- solari_e2b_compat-0.2.0/LICENSE +21 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/PKG-INFO +2 -1
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/pyproject.toml +2 -2
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/__init__.py +1 -1
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_config.py +4 -1
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_sandbox.py +51 -8
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/.gitignore +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/LICENSE-E2B +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/NOTICE +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/README.md +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_box.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_commands.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_filesystem.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_loop.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_sync.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/_template.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/code_interpreter.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/exceptions.py +0 -0
- {solari_e2b_compat-0.1.0 → solari_e2b_compat-0.2.0}/solari_e2b/models.py +0 -0
|
@@ -41,7 +41,7 @@ and the differences that are specific to Python.
|
|
|
41
41
|
| `get_metrics()` | DIFF | One current sample (no history); `mem_cache` is 0. |
|
|
42
42
|
| `pause()` | OK, live | Keeps RAM and disk. `mode='filesystem'`, `keep_memory=False`: treated as a full pause. |
|
|
43
43
|
| `create_snapshot(name=)`, `list_snapshots(...)`, `Sandbox.delete_snapshot(id)` | OK, live | Snapshots are RAM + disk. `mode='filesystem'` is ignored. A name lookup reads at most 1000 snapshots. `sbx.list_snapshots()` lists snapshots taken of that sandbox. Class-level listing over every sandbox is `Sandbox.list_all_snapshots(name=)` (extension). |
|
|
44
|
-
| `fork(count=)` | DIFF, live | Checkpoint then `count` new sandboxes from it; the checkpoint is
|
|
44
|
+
| `fork(count=)` | DIFF, live | Checkpoint then `count` new sandboxes from it; the gateway will not delete a checkpoint while a copy is alive, so each copy carries the checkpoint id in a hidden metadata key (`e2b.forkSnapshot`, not shown in `info.metadata`; you cannot set it yourself) and the **last copy killed deletes it** (waiting up to ~40 s). A copy that is not killed (it expires on its idle timeout) keeps the checkpoint, which is billed snapshot storage: kill your copies. Returns a sandbox or an exception per copy. |
|
|
45
45
|
| `get_host(port)` | NO | E2B returns the host synchronously; Solari mints preview URLs on the gateway. Use the extension `sbx.get_preview_url(port)` (returns `{'url': ..., 'token': ...}`). |
|
|
46
46
|
| `upload_url`, `download_url` | OK | `user=`: NO. |
|
|
47
47
|
| `update_network`, `get_mcp_url`, `get_mcp_token` (returns `None`), `git` | NO | `git` is deprecated in E2B; use `commands.run('git …')`. |
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Solari Desktop
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: solari-e2b-compat
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: E2B-compatible Python interface (sandboxes and code interpreter) over Solari Sandboxes. Not affiliated with E2B.
|
|
5
5
|
Project-URL: Homepage, https://getsolari.com
|
|
6
6
|
Author: Solari
|
|
7
7
|
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
License-File: LICENSE-E2B
|
|
9
10
|
License-File: NOTICE
|
|
10
11
|
Keywords: code-interpreter,compatibility,e2b,sandbox,solari
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "solari-e2b-compat"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "E2B-compatible Python interface (sandboxes and code interpreter) over Solari Sandboxes. Not affiliated with E2B."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -23,7 +23,7 @@ Homepage = "https://getsolari.com"
|
|
|
23
23
|
packages = ["solari_e2b"]
|
|
24
24
|
|
|
25
25
|
[tool.hatch.build.targets.sdist]
|
|
26
|
-
include = ["solari_e2b", "README.md", "COMPAT.md", "NOTICE", "LICENSE-E2B"]
|
|
26
|
+
include = ["solari_e2b", "README.md", "COMPAT.md", "NOTICE", "LICENSE", "LICENSE-E2B"]
|
|
27
27
|
|
|
28
28
|
[tool.pytest.ini_options]
|
|
29
29
|
asyncio_mode = "auto"
|
|
@@ -99,7 +99,10 @@ def _resolve(opts: Dict[str, Any]) -> Tuple[str, str]:
|
|
|
99
99
|
_describe(inherited_raw),
|
|
100
100
|
DEFAULT_BASE_URL,
|
|
101
101
|
)
|
|
102
|
-
|
|
102
|
+
env_base = os.environ.get("SOLARI_BASE_URL")
|
|
103
|
+
if not base and env_base and not is_solari_base_url(env_base):
|
|
104
|
+
raise SandboxException(f"Refusing to send a Solari API key to {_describe(env_base)}: it does not point at Solari.")
|
|
105
|
+
return key, (base or env_base or inherited or DEFAULT_BASE_URL).rstrip("/")
|
|
103
106
|
|
|
104
107
|
|
|
105
108
|
def resolve_conn(**opts: Any) -> Conn:
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import functools
|
|
7
7
|
import inspect
|
|
8
|
+
import logging
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from typing import Any, Callable, Dict, List, Optional
|
|
10
11
|
from urllib.parse import quote, urlencode
|
|
@@ -35,13 +36,46 @@ def _parse_time(s: Optional[str]) -> datetime:
|
|
|
35
36
|
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
log = logging.getLogger("solari_e2b")
|
|
40
|
+
|
|
41
|
+
#: Reserved metadata key (hidden from ``info.metadata``): the checkpoint a fork was booted from, deleted with the last copy.
|
|
42
|
+
FORK_KEY = "e2b.forkSnapshot"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _public_metadata(m: Optional[Dict[str, str]]) -> Dict[str, str]:
|
|
46
|
+
return {k: v for k, v in (m or {}).items() if k != FORK_KEY}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _release_fork_snapshot(conn: Any, snapshot_id: str, self_id: str) -> None:
|
|
50
|
+
"""Delete a fork's checkpoint once no copy that needs it is left. The gateway refuses while a copy is alive, so this runs
|
|
51
|
+
after the last copy is killed (it waits for that sandbox's release to settle, about 40 s at most) and says so if it fails."""
|
|
52
|
+
try:
|
|
53
|
+
listed = await conn.solari.list(metadata={FORK_KEY: snapshot_id}, limit=50)
|
|
54
|
+
views = [_view_dict(x) for x in listed["sandboxes"]]
|
|
55
|
+
if any(v.get("sandboxId") != self_id and v.get("state") not in ("gone", "releasing") for v in views):
|
|
56
|
+
return # another live copy still needs it; the last one to be killed deletes it (one that is only "releasing" no longer does)
|
|
57
|
+
last: Optional[BaseException] = None
|
|
58
|
+
for _ in range(20):
|
|
59
|
+
try:
|
|
60
|
+
await conn.solari.delete_snapshot(snapshot_id)
|
|
61
|
+
return
|
|
62
|
+
except Exception as e: # noqa: BLE001
|
|
63
|
+
if getattr(e, "status", None) == 404: # another copy killed at the same time already deleted it
|
|
64
|
+
return
|
|
65
|
+
last = e
|
|
66
|
+
await asyncio.sleep(2)
|
|
67
|
+
log.warning("could not delete the fork checkpoint %s (%s); delete it with Sandbox.delete_snapshot(%r)", snapshot_id, last, snapshot_id)
|
|
68
|
+
except Exception: # noqa: BLE001
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
38
72
|
def to_info(v: Dict[str, Any]) -> SandboxInfo:
|
|
39
73
|
state = "paused" if v.get("state") in ("paused", "stopped") else "running"
|
|
40
74
|
return SandboxInfo(
|
|
41
75
|
sandbox_id=v["sandboxId"],
|
|
42
76
|
template_id="",
|
|
43
77
|
name=v.get("name"),
|
|
44
|
-
metadata=v.get("metadata")
|
|
78
|
+
metadata=_public_metadata(v.get("metadata")),
|
|
45
79
|
started_at=_parse_time(v.get("createdAt") or v.get("startedAt")),
|
|
46
80
|
end_at=_parse_time(v.get("expiresAt")),
|
|
47
81
|
state=state,
|
|
@@ -224,6 +258,9 @@ class AsyncSandbox:
|
|
|
224
258
|
raise SolariCompatUnsupported("iam")
|
|
225
259
|
if allow_internet_access is False:
|
|
226
260
|
raise SolariCompatUnsupported("allow_internet_access=False", "Solari sandboxes always have internet access")
|
|
261
|
+
# A caller could otherwise name another sandbox's checkpoint here, and killing this sandbox would then delete it.
|
|
262
|
+
if metadata and FORK_KEY in metadata:
|
|
263
|
+
raise SolariCompatUnsupported(f"metadata key {FORK_KEY!r}", "this key is reserved by the compatibility layer")
|
|
227
264
|
conn = resolve_conn(**split_opts(opts)[0])
|
|
228
265
|
volumes = await _resolve_volumes(conn, volume_mounts) if volume_mounts else None
|
|
229
266
|
try:
|
|
@@ -280,8 +317,11 @@ class AsyncSandbox:
|
|
|
280
317
|
# The gateway's DELETE is idempotent; E2B reports False for a sandbox that is already gone.
|
|
281
318
|
await self._box.close()
|
|
282
319
|
try:
|
|
283
|
-
await get_view(self._box.conn, self._sandbox_id)
|
|
320
|
+
view = await get_view(self._box.conn, self._sandbox_id)
|
|
284
321
|
await self._box.conn.solari.kill(self._sandbox_id)
|
|
322
|
+
fork = (view.get("metadata") or {}).get(FORK_KEY)
|
|
323
|
+
if fork:
|
|
324
|
+
await _release_fork_snapshot(self._box.conn, fork, self._sandbox_id)
|
|
285
325
|
return True
|
|
286
326
|
except Exception as e: # noqa: BLE001
|
|
287
327
|
t = translate(e, "sandbox")
|
|
@@ -379,23 +419,26 @@ class AsyncSandbox:
|
|
|
379
419
|
try:
|
|
380
420
|
view = await get_view(conn, self._sandbox_id)
|
|
381
421
|
snap = (await self.create_snapshot()).snapshot_id
|
|
422
|
+
parent_md = _public_metadata(view.get("metadata"))
|
|
382
423
|
except Exception as e: # noqa: BLE001
|
|
383
424
|
raise translate(e, "sandbox") from e
|
|
384
425
|
|
|
385
426
|
async def one() -> Any:
|
|
386
427
|
try:
|
|
387
|
-
core = await conn.solari.create(from_snapshot=snap, timeout_ms=int((timeout if timeout is not None else DEFAULT_TIMEOUT) * 1000), metadata=
|
|
428
|
+
core = await conn.solari.create(from_snapshot=snap, timeout_ms=int((timeout if timeout is not None else DEFAULT_TIMEOUT) * 1000), metadata={**parent_md, FORK_KEY: snap})
|
|
388
429
|
return cls(conn, core.sandboxId, core)
|
|
389
430
|
except Exception as e: # noqa: BLE001
|
|
390
431
|
t = translate(e)
|
|
391
432
|
return t if isinstance(t, BaseException) else SandboxException(str(t))
|
|
392
433
|
|
|
393
434
|
forks = await asyncio.gather(*(one() for _ in range(n)))
|
|
394
|
-
# The checkpoint was made for this call only
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
435
|
+
# The checkpoint was made for this call only. While a copy is alive the gateway refuses to delete it; the copies carry its
|
|
436
|
+
# id (FORK_KEY) and the last one killed deletes it (see ``kill``). If no copy came up there is nothing to wait for.
|
|
437
|
+
if all(isinstance(f, BaseException) for f in forks):
|
|
438
|
+
try:
|
|
439
|
+
await asyncio.wait_for(conn.solari.delete_snapshot(snap), 20)
|
|
440
|
+
except Exception: # noqa: BLE001
|
|
441
|
+
pass
|
|
399
442
|
return list(forks)
|
|
400
443
|
|
|
401
444
|
# --- network / urls ---
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|