roche-sandbox 0.1.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.
- roche_sandbox-0.1.0/.gitignore +6 -0
- roche_sandbox-0.1.0/PKG-INFO +122 -0
- roche_sandbox-0.1.0/README.md +96 -0
- roche_sandbox-0.1.0/pyproject.toml +45 -0
- roche_sandbox-0.1.0/scripts/proto-gen.sh +18 -0
- roche_sandbox-0.1.0/src/roche_sandbox/__init__.py +33 -0
- roche_sandbox-0.1.0/src/roche_sandbox/client.py +118 -0
- roche_sandbox-0.1.0/src/roche_sandbox/daemon.py +40 -0
- roche_sandbox-0.1.0/src/roche_sandbox/errors.py +28 -0
- roche_sandbox-0.1.0/src/roche_sandbox/sandbox.py +85 -0
- roche_sandbox-0.1.0/src/roche_sandbox/transport/__init__.py +17 -0
- roche_sandbox-0.1.0/src/roche_sandbox/transport/cli.py +125 -0
- roche_sandbox-0.1.0/src/roche_sandbox/transport/grpc.py +129 -0
- roche_sandbox-0.1.0/src/roche_sandbox/types.py +44 -0
- roche_sandbox-0.1.0/tests/__init__.py +0 -0
- roche_sandbox-0.1.0/tests/integration/__init__.py +0 -0
- roche_sandbox-0.1.0/tests/integration/conftest.py +36 -0
- roche_sandbox-0.1.0/tests/integration/test_cli_e2e.py +73 -0
- roche_sandbox-0.1.0/tests/test_client.py +90 -0
- roche_sandbox-0.1.0/tests/test_daemon.py +37 -0
- roche_sandbox-0.1.0/tests/test_errors.py +25 -0
- roche_sandbox-0.1.0/tests/test_sandbox.py +87 -0
- roche_sandbox-0.1.0/tests/test_types.py +24 -0
- roche_sandbox-0.1.0/tests/transport/__init__.py +0 -0
- roche_sandbox-0.1.0/tests/transport/test_cli.py +170 -0
- roche_sandbox-0.1.0/tests/transport/test_grpc.py +59 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: roche-sandbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal sandbox orchestrator for AI agents — Python SDK
|
|
5
|
+
Project-URL: Homepage, https://github.com/substratum-labs/roche
|
|
6
|
+
Project-URL: Repository, https://github.com/substratum-labs/roche
|
|
7
|
+
Author: Substratum Labs
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Keywords: agent,ai,docker,orchestrator,sandbox,wasm
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: System :: Emulators
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: grpcio>=1.60.0
|
|
20
|
+
Requires-Dist: protobuf>=4.25.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: grpcio-tools>=1.60.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# roche-sandbox
|
|
28
|
+
|
|
29
|
+
Python SDK for [Roche](https://github.com/substratum-labs/roche) -- universal sandbox orchestrator for AI agents.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Python >= 3.10
|
|
34
|
+
- Roche CLI on `PATH` (or Roche daemon running)
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install roche-sandbox
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from roche_sandbox import Roche
|
|
46
|
+
|
|
47
|
+
roche = Roche()
|
|
48
|
+
sandbox = roche.create(image="python:3.12-slim")
|
|
49
|
+
output = sandbox.exec(["python3", "-c", "print('Hello from Roche!')"])
|
|
50
|
+
print(output.stdout) # Hello from Roche!
|
|
51
|
+
sandbox.destroy()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Context Manager (auto-cleanup)
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
with roche.create(image="python:3.12-slim") as sandbox:
|
|
58
|
+
output = sandbox.exec(["echo", "hello"])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Async API
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
from roche_sandbox import AsyncRoche
|
|
66
|
+
|
|
67
|
+
async def main():
|
|
68
|
+
roche = AsyncRoche()
|
|
69
|
+
sandbox = await roche.create(image="python:3.12-slim")
|
|
70
|
+
output = await sandbox.exec(["echo", "hello"])
|
|
71
|
+
await sandbox.destroy()
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
sandbox = roche.create(
|
|
80
|
+
image="python:3.12-slim",
|
|
81
|
+
memory="512m",
|
|
82
|
+
cpus=1.0,
|
|
83
|
+
timeout_secs=600,
|
|
84
|
+
network=False, # default: AI-safe
|
|
85
|
+
writable=False, # default: AI-safe
|
|
86
|
+
env={"API_KEY": "secret"},
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Transport
|
|
91
|
+
|
|
92
|
+
The SDK auto-detects whether the Roche gRPC daemon is running and connects to it. If the daemon is unavailable, it falls back to invoking the Roche CLI as a subprocess.
|
|
93
|
+
|
|
94
|
+
You can force CLI mode explicitly:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
roche = Roche(mode="direct")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## API Styles
|
|
101
|
+
|
|
102
|
+
The SDK provides two API styles:
|
|
103
|
+
|
|
104
|
+
- **Async-first**: `AsyncRoche` and `AsyncSandbox` -- native `async`/`await` support.
|
|
105
|
+
- **Sync wrapper**: `Roche` and `Sandbox` -- blocking equivalents for scripts and notebooks.
|
|
106
|
+
|
|
107
|
+
## Public Exports
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from roche_sandbox import (
|
|
111
|
+
Roche, AsyncRoche,
|
|
112
|
+
Sandbox, AsyncSandbox,
|
|
113
|
+
SandboxConfig, ExecOutput, SandboxInfo,
|
|
114
|
+
Mount, SandboxStatus,
|
|
115
|
+
RocheError, SandboxNotFound, SandboxPaused,
|
|
116
|
+
ProviderUnavailable, TimeoutError, UnsupportedOperation,
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
Apache-2.0 -- see [LICENSE](https://github.com/substratum-labs/roche/blob/main/LICENSE).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# roche-sandbox
|
|
2
|
+
|
|
3
|
+
Python SDK for [Roche](https://github.com/substratum-labs/roche) -- universal sandbox orchestrator for AI agents.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python >= 3.10
|
|
8
|
+
- Roche CLI on `PATH` (or Roche daemon running)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install roche-sandbox
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from roche_sandbox import Roche
|
|
20
|
+
|
|
21
|
+
roche = Roche()
|
|
22
|
+
sandbox = roche.create(image="python:3.12-slim")
|
|
23
|
+
output = sandbox.exec(["python3", "-c", "print('Hello from Roche!')"])
|
|
24
|
+
print(output.stdout) # Hello from Roche!
|
|
25
|
+
sandbox.destroy()
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Context Manager (auto-cleanup)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
with roche.create(image="python:3.12-slim") as sandbox:
|
|
32
|
+
output = sandbox.exec(["echo", "hello"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Async API
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from roche_sandbox import AsyncRoche
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
roche = AsyncRoche()
|
|
43
|
+
sandbox = await roche.create(image="python:3.12-slim")
|
|
44
|
+
output = await sandbox.exec(["echo", "hello"])
|
|
45
|
+
await sandbox.destroy()
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
sandbox = roche.create(
|
|
54
|
+
image="python:3.12-slim",
|
|
55
|
+
memory="512m",
|
|
56
|
+
cpus=1.0,
|
|
57
|
+
timeout_secs=600,
|
|
58
|
+
network=False, # default: AI-safe
|
|
59
|
+
writable=False, # default: AI-safe
|
|
60
|
+
env={"API_KEY": "secret"},
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Transport
|
|
65
|
+
|
|
66
|
+
The SDK auto-detects whether the Roche gRPC daemon is running and connects to it. If the daemon is unavailable, it falls back to invoking the Roche CLI as a subprocess.
|
|
67
|
+
|
|
68
|
+
You can force CLI mode explicitly:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
roche = Roche(mode="direct")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API Styles
|
|
75
|
+
|
|
76
|
+
The SDK provides two API styles:
|
|
77
|
+
|
|
78
|
+
- **Async-first**: `AsyncRoche` and `AsyncSandbox` -- native `async`/`await` support.
|
|
79
|
+
- **Sync wrapper**: `Roche` and `Sandbox` -- blocking equivalents for scripts and notebooks.
|
|
80
|
+
|
|
81
|
+
## Public Exports
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from roche_sandbox import (
|
|
85
|
+
Roche, AsyncRoche,
|
|
86
|
+
Sandbox, AsyncSandbox,
|
|
87
|
+
SandboxConfig, ExecOutput, SandboxInfo,
|
|
88
|
+
Mount, SandboxStatus,
|
|
89
|
+
RocheError, SandboxNotFound, SandboxPaused,
|
|
90
|
+
ProviderUnavailable, TimeoutError, UnsupportedOperation,
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
Apache-2.0 -- see [LICENSE](https://github.com/substratum-labs/roche/blob/main/LICENSE).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "roche-sandbox"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Universal sandbox orchestrator for AI agents — Python SDK"
|
|
9
|
+
license = {text = "Apache-2.0"}
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
keywords = ["sandbox", "docker", "ai", "agent", "wasm", "orchestrator"]
|
|
13
|
+
authors = [{name = "Substratum Labs"}]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
"Topic :: System :: Emulators",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"grpcio>=1.60.0",
|
|
26
|
+
"protobuf>=4.25.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/roche_sandbox"]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=7.0",
|
|
35
|
+
"pytest-asyncio>=0.23.0",
|
|
36
|
+
"grpcio-tools>=1.60.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
asyncio_mode = "strict"
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/substratum-labs/roche"
|
|
45
|
+
Repository = "https://github.com/substratum-labs/roche"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PROTO_DIR="$(cd "$(dirname "$0")/../../.." && pwd)/proto"
|
|
5
|
+
OUT_DIR="$(cd "$(dirname "$0")/.." && pwd)/src/roche_sandbox/generated"
|
|
6
|
+
|
|
7
|
+
rm -rf "$OUT_DIR"
|
|
8
|
+
mkdir -p "$OUT_DIR/roche/v1"
|
|
9
|
+
touch "$OUT_DIR/__init__.py"
|
|
10
|
+
touch "$OUT_DIR/roche/__init__.py"
|
|
11
|
+
touch "$OUT_DIR/roche/v1/__init__.py"
|
|
12
|
+
|
|
13
|
+
python -m grpc_tools.protoc \
|
|
14
|
+
-I "$PROTO_DIR" \
|
|
15
|
+
--python_out="$OUT_DIR" \
|
|
16
|
+
--grpc_python_out="$OUT_DIR" \
|
|
17
|
+
--pyi_out="$OUT_DIR" \
|
|
18
|
+
"$PROTO_DIR/roche/v1/sandbox.proto"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Roche — Universal sandbox orchestrator for AI agents (Python SDK)."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from roche_sandbox.client import AsyncRoche, Roche
|
|
6
|
+
from roche_sandbox.errors import (
|
|
7
|
+
ProviderUnavailable,
|
|
8
|
+
RocheError,
|
|
9
|
+
SandboxNotFound,
|
|
10
|
+
SandboxPaused,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
UnsupportedOperation,
|
|
13
|
+
)
|
|
14
|
+
from roche_sandbox.sandbox import AsyncSandbox, Sandbox
|
|
15
|
+
from roche_sandbox.types import ExecOutput, Mount, SandboxConfig, SandboxInfo, SandboxStatus
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AsyncRoche",
|
|
19
|
+
"Roche",
|
|
20
|
+
"AsyncSandbox",
|
|
21
|
+
"Sandbox",
|
|
22
|
+
"RocheError",
|
|
23
|
+
"SandboxNotFound",
|
|
24
|
+
"SandboxPaused",
|
|
25
|
+
"ProviderUnavailable",
|
|
26
|
+
"TimeoutError",
|
|
27
|
+
"UnsupportedOperation",
|
|
28
|
+
"SandboxConfig",
|
|
29
|
+
"ExecOutput",
|
|
30
|
+
"SandboxInfo",
|
|
31
|
+
"SandboxStatus",
|
|
32
|
+
"Mount",
|
|
33
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from roche_sandbox.daemon import detect_daemon
|
|
7
|
+
from roche_sandbox.sandbox import AsyncSandbox, Sandbox
|
|
8
|
+
from roche_sandbox.transport.cli import CliTransport
|
|
9
|
+
from roche_sandbox.transport.grpc import GrpcTransport
|
|
10
|
+
from roche_sandbox.types import ExecOutput, SandboxConfig, SandboxInfo
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from roche_sandbox.transport import Transport
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncRoche:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
mode: str = "auto",
|
|
21
|
+
daemon_port: int | None = None,
|
|
22
|
+
provider: str = "docker",
|
|
23
|
+
binary: str = "roche",
|
|
24
|
+
transport: Transport | None = None,
|
|
25
|
+
):
|
|
26
|
+
self._provider = provider
|
|
27
|
+
if transport is not None:
|
|
28
|
+
self._transport = transport
|
|
29
|
+
elif mode == "direct":
|
|
30
|
+
self._transport = CliTransport(binary=binary)
|
|
31
|
+
elif daemon_port is not None:
|
|
32
|
+
self._transport = GrpcTransport(port=daemon_port)
|
|
33
|
+
else:
|
|
34
|
+
daemon = detect_daemon()
|
|
35
|
+
if daemon is not None:
|
|
36
|
+
self._transport = GrpcTransport(port=daemon["port"])
|
|
37
|
+
else:
|
|
38
|
+
self._transport = CliTransport(binary=binary)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def transport(self) -> Transport:
|
|
42
|
+
return self._transport
|
|
43
|
+
|
|
44
|
+
async def create(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
provider: str | None = None,
|
|
48
|
+
image: str = "python:3.12-slim",
|
|
49
|
+
memory: str | None = None,
|
|
50
|
+
cpus: float | None = None,
|
|
51
|
+
timeout_secs: int = 300,
|
|
52
|
+
network: bool = False,
|
|
53
|
+
writable: bool = False,
|
|
54
|
+
env: dict[str, str] | None = None,
|
|
55
|
+
mounts: list | None = None,
|
|
56
|
+
kernel: str | None = None,
|
|
57
|
+
rootfs: str | None = None,
|
|
58
|
+
) -> AsyncSandbox:
|
|
59
|
+
effective_provider = provider or self._provider
|
|
60
|
+
config = SandboxConfig(
|
|
61
|
+
provider=effective_provider,
|
|
62
|
+
image=image,
|
|
63
|
+
memory=memory,
|
|
64
|
+
cpus=cpus,
|
|
65
|
+
timeout_secs=timeout_secs,
|
|
66
|
+
network=network,
|
|
67
|
+
writable=writable,
|
|
68
|
+
env=env or {},
|
|
69
|
+
mounts=mounts or [],
|
|
70
|
+
kernel=kernel,
|
|
71
|
+
rootfs=rootfs,
|
|
72
|
+
)
|
|
73
|
+
sandbox_id = await self._transport.create(config, effective_provider)
|
|
74
|
+
return AsyncSandbox(sandbox_id, effective_provider, self._transport)
|
|
75
|
+
|
|
76
|
+
async def create_id(self, **kwargs) -> str:
|
|
77
|
+
sb = await self.create(**kwargs)
|
|
78
|
+
return sb.id
|
|
79
|
+
|
|
80
|
+
async def exec(
|
|
81
|
+
self, sandbox_id: str, command: list[str], timeout_secs: int | None = None
|
|
82
|
+
) -> ExecOutput:
|
|
83
|
+
return await self._transport.exec(sandbox_id, command, self._provider, timeout_secs)
|
|
84
|
+
|
|
85
|
+
async def destroy(self, sandbox_id: str) -> None:
|
|
86
|
+
await self._transport.destroy([sandbox_id], self._provider)
|
|
87
|
+
|
|
88
|
+
async def list(self) -> list[SandboxInfo]:
|
|
89
|
+
return await self._transport.list(self._provider)
|
|
90
|
+
|
|
91
|
+
async def gc(self, dry_run: bool = False, all: bool = False) -> list[str]:
|
|
92
|
+
return await self._transport.gc(self._provider, dry_run, all)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Roche:
|
|
96
|
+
def __init__(self, **kwargs):
|
|
97
|
+
self._async = AsyncRoche(**kwargs)
|
|
98
|
+
|
|
99
|
+
def create(self, **kwargs) -> Sandbox:
|
|
100
|
+
sb = asyncio.run(self._async.create(**kwargs))
|
|
101
|
+
return Sandbox(sb.id, sb.provider, self._async.transport)
|
|
102
|
+
|
|
103
|
+
def create_id(self, **kwargs) -> str:
|
|
104
|
+
return asyncio.run(self._async.create_id(**kwargs))
|
|
105
|
+
|
|
106
|
+
def exec(
|
|
107
|
+
self, sandbox_id: str, command: list[str], timeout_secs: int | None = None
|
|
108
|
+
) -> ExecOutput:
|
|
109
|
+
return asyncio.run(self._async.exec(sandbox_id, command, timeout_secs))
|
|
110
|
+
|
|
111
|
+
def destroy(self, sandbox_id: str) -> None:
|
|
112
|
+
asyncio.run(self._async.destroy(sandbox_id))
|
|
113
|
+
|
|
114
|
+
def list(self) -> list[SandboxInfo]:
|
|
115
|
+
return asyncio.run(self._async.list())
|
|
116
|
+
|
|
117
|
+
def gc(self, dry_run: bool = False, all: bool = False) -> list[str]:
|
|
118
|
+
return asyncio.run(self._async.gc(dry_run, all))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DaemonInfo(TypedDict):
|
|
10
|
+
pid: int
|
|
11
|
+
port: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def daemon_json_path() -> Path:
|
|
15
|
+
return Path.home() / ".roche" / "daemon.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def detect_daemon() -> DaemonInfo | None:
|
|
19
|
+
path = daemon_json_path()
|
|
20
|
+
if not path.exists():
|
|
21
|
+
return None
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(path.read_text())
|
|
24
|
+
except (json.JSONDecodeError, OSError):
|
|
25
|
+
return None
|
|
26
|
+
pid = data.get("pid")
|
|
27
|
+
port = data.get("port")
|
|
28
|
+
if not isinstance(pid, int) or not isinstance(port, int):
|
|
29
|
+
return None
|
|
30
|
+
if not _is_process_alive(pid):
|
|
31
|
+
return None
|
|
32
|
+
return DaemonInfo(pid=pid, port=port)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_process_alive(pid: int) -> bool:
|
|
36
|
+
try:
|
|
37
|
+
os.kill(pid, 0)
|
|
38
|
+
return True
|
|
39
|
+
except (OSError, ProcessLookupError):
|
|
40
|
+
return False
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class RocheError(Exception):
|
|
2
|
+
def __init__(self, message: str):
|
|
3
|
+
super().__init__(message)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SandboxNotFound(RocheError):
|
|
7
|
+
def __init__(self, detail: str):
|
|
8
|
+
super().__init__(f"Sandbox not found: {detail}")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SandboxPaused(RocheError):
|
|
12
|
+
def __init__(self, detail: str):
|
|
13
|
+
super().__init__(f"Sandbox is paused: {detail}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProviderUnavailable(RocheError):
|
|
17
|
+
def __init__(self, detail: str):
|
|
18
|
+
super().__init__(f"Provider unavailable: {detail}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TimeoutError(RocheError):
|
|
22
|
+
def __init__(self, detail: str):
|
|
23
|
+
super().__init__(f"Operation timed out: {detail}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UnsupportedOperation(RocheError):
|
|
27
|
+
def __init__(self, detail: str):
|
|
28
|
+
super().__init__(f"Unsupported operation: {detail}")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from roche_sandbox.types import ExecOutput
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from roche_sandbox.transport import Transport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncSandbox:
|
|
13
|
+
def __init__(self, id: str, provider: str, transport: Transport):
|
|
14
|
+
self._id = id
|
|
15
|
+
self._provider = provider
|
|
16
|
+
self._transport = transport
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def id(self) -> str:
|
|
20
|
+
return self._id
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def provider(self) -> str:
|
|
24
|
+
return self._provider
|
|
25
|
+
|
|
26
|
+
async def exec(self, command: list[str], timeout_secs: int | None = None) -> ExecOutput:
|
|
27
|
+
return await self._transport.exec(self._id, command, self._provider, timeout_secs)
|
|
28
|
+
|
|
29
|
+
async def pause(self) -> None:
|
|
30
|
+
await self._transport.pause(self._id, self._provider)
|
|
31
|
+
|
|
32
|
+
async def unpause(self) -> None:
|
|
33
|
+
await self._transport.unpause(self._id, self._provider)
|
|
34
|
+
|
|
35
|
+
async def destroy(self) -> None:
|
|
36
|
+
await self._transport.destroy([self._id], self._provider)
|
|
37
|
+
|
|
38
|
+
async def copy_to(self, host_path: str, sandbox_path: str) -> None:
|
|
39
|
+
await self._transport.copy_to(self._id, host_path, sandbox_path, self._provider)
|
|
40
|
+
|
|
41
|
+
async def copy_from(self, sandbox_path: str, host_path: str) -> None:
|
|
42
|
+
await self._transport.copy_from(self._id, sandbox_path, host_path, self._provider)
|
|
43
|
+
|
|
44
|
+
async def __aenter__(self) -> AsyncSandbox:
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
48
|
+
await self.destroy()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Sandbox:
|
|
52
|
+
def __init__(self, id: str, provider: str, transport: Transport):
|
|
53
|
+
self._inner = AsyncSandbox(id, provider, transport)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def id(self) -> str:
|
|
57
|
+
return self._inner.id
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def provider(self) -> str:
|
|
61
|
+
return self._inner.provider
|
|
62
|
+
|
|
63
|
+
def exec(self, command: list[str], timeout_secs: int | None = None) -> ExecOutput:
|
|
64
|
+
return asyncio.run(self._inner.exec(command, timeout_secs))
|
|
65
|
+
|
|
66
|
+
def pause(self) -> None:
|
|
67
|
+
asyncio.run(self._inner.pause())
|
|
68
|
+
|
|
69
|
+
def unpause(self) -> None:
|
|
70
|
+
asyncio.run(self._inner.unpause())
|
|
71
|
+
|
|
72
|
+
def destroy(self) -> None:
|
|
73
|
+
asyncio.run(self._inner.destroy())
|
|
74
|
+
|
|
75
|
+
def copy_to(self, host_path: str, sandbox_path: str) -> None:
|
|
76
|
+
asyncio.run(self._inner.copy_to(host_path, sandbox_path))
|
|
77
|
+
|
|
78
|
+
def copy_from(self, sandbox_path: str, host_path: str) -> None:
|
|
79
|
+
asyncio.run(self._inner.copy_from(sandbox_path, host_path))
|
|
80
|
+
|
|
81
|
+
def __enter__(self) -> Sandbox:
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def __exit__(self, *exc: object) -> None:
|
|
85
|
+
self.destroy()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from roche_sandbox.types import ExecOutput, SandboxConfig, SandboxInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Transport(Protocol):
|
|
9
|
+
async def create(self, config: SandboxConfig, provider: str) -> str: ...
|
|
10
|
+
async def exec(self, sandbox_id: str, command: list[str], provider: str, timeout_secs: int | None = None) -> ExecOutput: ...
|
|
11
|
+
async def destroy(self, sandbox_ids: list[str], provider: str, all: bool = False) -> list[str]: ...
|
|
12
|
+
async def list(self, provider: str) -> list[SandboxInfo]: ...
|
|
13
|
+
async def pause(self, sandbox_id: str, provider: str) -> None: ...
|
|
14
|
+
async def unpause(self, sandbox_id: str, provider: str) -> None: ...
|
|
15
|
+
async def gc(self, provider: str, dry_run: bool = False, all: bool = False) -> list[str]: ...
|
|
16
|
+
async def copy_to(self, sandbox_id: str, host_path: str, sandbox_path: str, provider: str) -> None: ...
|
|
17
|
+
async def copy_from(self, sandbox_id: str, sandbox_path: str, host_path: str, provider: str) -> None: ...
|