opencomputer-sdk 0.3.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.
@@ -0,0 +1,49 @@
1
+ # OS files
2
+ .DS_Store
3
+ Thumbs.db
4
+
5
+ # Editor/IDE files
6
+ .cursor/
7
+ *.swp
8
+ *.swo
9
+ *~
10
+ .idea/
11
+ .vscode/
12
+
13
+ # Environment files
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+
18
+ # Python
19
+ __pycache__/
20
+ *.pyc
21
+ *.pyo
22
+ *.egg-info/
23
+ .pytest_cache/
24
+
25
+ # Node
26
+ node_modules/
27
+
28
+ # Build artifacts
29
+ dist/
30
+
31
+ # Compiled binaries
32
+ bin/opensandbox-server
33
+ bin/opensandbox-worker
34
+ opensandbox-worker
35
+
36
+ # Worker env (secrets)
37
+ worker.env
38
+ caddy.env
39
+
40
+ # Loom demo (private)
41
+ sdks/typescript/examples/loom/
42
+
43
+ # Media files
44
+ *.mp4
45
+ *.gif
46
+
47
+ # Temp files
48
+ *.tmp
49
+ *.log
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencomputer-sdk
3
+ Version: 0.3.0
4
+ Summary: Python SDK for OpenComputer - cloud sandbox platform
5
+ Project-URL: Homepage, https://github.com/diggerhq/opensandbox
6
+ Project-URL: Repository, https://github.com/diggerhq/opensandbox
7
+ Project-URL: Documentation, https://github.com/diggerhq/opensandbox/tree/main/sdks/python
8
+ Project-URL: Issues, https://github.com/diggerhq/opensandbox/issues
9
+ Author: OpenComputer
10
+ License-Expression: MIT
11
+ Keywords: cloud,code-execution,containers,opencomputer,sandbox
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: websockets>=12.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # opencomputer
30
+
31
+ Python SDK for [OpenComputer](https://github.com/diggerhq/opensandbox) — cloud sandbox platform.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install opencomputer
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ import asyncio
43
+ from opencomputer import Sandbox
44
+
45
+ async def main():
46
+ sandbox = await Sandbox.create(template="base")
47
+
48
+ # Execute commands
49
+ result = await sandbox.commands.run("echo hello")
50
+ print(result.stdout) # "hello\n"
51
+
52
+ # Read and write files
53
+ await sandbox.files.write("/tmp/test.txt", "Hello, world!")
54
+ content = await sandbox.files.read("/tmp/test.txt")
55
+
56
+ # Clean up
57
+ await sandbox.kill()
58
+ await sandbox.close()
59
+
60
+ asyncio.run(main())
61
+ ```
62
+
63
+ ## Configuration
64
+
65
+ | Parameter | Env Variable | Default |
66
+ |------------|------------------------|-------------------------|
67
+ | `api_url` | `OPENSANDBOX_API_URL` | `https://app.opencomputer.dev` |
68
+ | `api_key` | `OPENSANDBOX_API_KEY` | (none) |
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,44 @@
1
+ # opencomputer
2
+
3
+ Python SDK for [OpenComputer](https://github.com/diggerhq/opensandbox) — cloud sandbox platform.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install opencomputer
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import asyncio
15
+ from opencomputer import Sandbox
16
+
17
+ async def main():
18
+ sandbox = await Sandbox.create(template="base")
19
+
20
+ # Execute commands
21
+ result = await sandbox.commands.run("echo hello")
22
+ print(result.stdout) # "hello\n"
23
+
24
+ # Read and write files
25
+ await sandbox.files.write("/tmp/test.txt", "Hello, world!")
26
+ content = await sandbox.files.read("/tmp/test.txt")
27
+
28
+ # Clean up
29
+ await sandbox.kill()
30
+ await sandbox.close()
31
+
32
+ asyncio.run(main())
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ | Parameter | Env Variable | Default |
38
+ |------------|------------------------|-------------------------|
39
+ | `api_url` | `OPENSANDBOX_API_URL` | `https://app.opencomputer.dev` |
40
+ | `api_key` | `OPENSANDBOX_API_KEY` | (none) |
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,19 @@
1
+ """OpenComputer Python SDK - cloud sandbox platform."""
2
+
3
+ from opencomputer.sandbox import Sandbox
4
+ from opencomputer.filesystem import Filesystem
5
+ from opencomputer.commands import Commands, ProcessResult
6
+ from opencomputer.pty import Pty, PtySession
7
+ from opencomputer.template import Template
8
+
9
+ __all__ = [
10
+ "Sandbox",
11
+ "Filesystem",
12
+ "Commands",
13
+ "ProcessResult",
14
+ "Pty",
15
+ "PtySession",
16
+ "Template",
17
+ ]
18
+
19
+ __version__ = "0.3.0"
@@ -0,0 +1,56 @@
1
+ """Command execution inside a sandbox."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+
11
+ @dataclass
12
+ class ProcessResult:
13
+ """Result of a command execution."""
14
+
15
+ exit_code: int
16
+ stdout: str
17
+ stderr: str
18
+
19
+
20
+ @dataclass
21
+ class Commands:
22
+ """Command execution for a sandbox."""
23
+
24
+ _client: httpx.AsyncClient
25
+ _sandbox_id: str
26
+
27
+ async def run(
28
+ self,
29
+ command: str,
30
+ timeout: int = 60,
31
+ env: dict[str, str] | None = None,
32
+ cwd: str | None = None,
33
+ ) -> ProcessResult:
34
+ """Run a command and wait for completion."""
35
+ body: dict[str, Any] = {
36
+ "cmd": command,
37
+ "timeout": timeout,
38
+ }
39
+ if env:
40
+ body["envs"] = env
41
+ if cwd:
42
+ body["cwd"] = cwd
43
+
44
+ resp = await self._client.post(
45
+ f"/sandboxes/{self._sandbox_id}/commands",
46
+ json=body,
47
+ timeout=timeout + 5,
48
+ )
49
+ resp.raise_for_status()
50
+ data = resp.json()
51
+
52
+ return ProcessResult(
53
+ exit_code=data.get("exitCode", -1),
54
+ stdout=data.get("stdout", ""),
55
+ stderr=data.get("stderr", ""),
56
+ )
@@ -0,0 +1,100 @@
1
+ """Filesystem operations inside a sandbox."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import httpx
8
+
9
+
10
+ @dataclass
11
+ class EntryInfo:
12
+ """File or directory entry."""
13
+
14
+ name: str
15
+ is_dir: bool
16
+ path: str
17
+ size: int = 0
18
+
19
+
20
+ @dataclass
21
+ class Filesystem:
22
+ """Filesystem operations for a sandbox."""
23
+
24
+ _client: httpx.AsyncClient
25
+ _sandbox_id: str
26
+
27
+ async def read(self, path: str) -> str:
28
+ """Read a file as text."""
29
+ resp = await self._client.get(
30
+ f"/sandboxes/{self._sandbox_id}/files",
31
+ params={"path": path},
32
+ )
33
+ resp.raise_for_status()
34
+ return resp.text
35
+
36
+ async def read_bytes(self, path: str) -> bytes:
37
+ """Read a file as bytes."""
38
+ resp = await self._client.get(
39
+ f"/sandboxes/{self._sandbox_id}/files",
40
+ params={"path": path},
41
+ )
42
+ resp.raise_for_status()
43
+ return resp.content
44
+
45
+ async def write(self, path: str, content: str | bytes) -> None:
46
+ """Write content to a file."""
47
+ data = content if isinstance(content, bytes) else content.encode()
48
+ resp = await self._client.put(
49
+ f"/sandboxes/{self._sandbox_id}/files",
50
+ params={"path": path},
51
+ content=data,
52
+ )
53
+ resp.raise_for_status()
54
+
55
+ async def list(self, path: str = "/") -> list[EntryInfo]:
56
+ """List directory contents."""
57
+ resp = await self._client.get(
58
+ f"/sandboxes/{self._sandbox_id}/files/list",
59
+ params={"path": path},
60
+ )
61
+ resp.raise_for_status()
62
+ data = resp.json()
63
+ if data is None:
64
+ return []
65
+ return [
66
+ EntryInfo(
67
+ name=entry["name"],
68
+ is_dir=entry.get("isDir", False),
69
+ path=entry.get("path", ""),
70
+ size=entry.get("size", 0),
71
+ )
72
+ for entry in data
73
+ ]
74
+
75
+ async def make_dir(self, path: str) -> None:
76
+ """Create a directory (and parents)."""
77
+ resp = await self._client.post(
78
+ f"/sandboxes/{self._sandbox_id}/files/mkdir",
79
+ params={"path": path},
80
+ )
81
+ resp.raise_for_status()
82
+
83
+ async def remove(self, path: str) -> None:
84
+ """Remove a file or directory."""
85
+ resp = await self._client.delete(
86
+ f"/sandboxes/{self._sandbox_id}/files",
87
+ params={"path": path},
88
+ )
89
+ resp.raise_for_status()
90
+
91
+ async def exists(self, path: str) -> bool:
92
+ """Check if a path exists."""
93
+ try:
94
+ resp = await self._client.get(
95
+ f"/sandboxes/{self._sandbox_id}/files",
96
+ params={"path": path},
97
+ )
98
+ return resp.status_code == 200
99
+ except httpx.HTTPError:
100
+ return False
@@ -0,0 +1,96 @@
1
+ """PTY terminal sessions inside a sandbox."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from dataclasses import dataclass
7
+ from typing import Callable
8
+
9
+ import httpx
10
+ import websockets
11
+
12
+
13
+ @dataclass
14
+ class PtySession:
15
+ """An active PTY terminal session."""
16
+
17
+ session_id: str
18
+ sandbox_id: str
19
+ _ws: websockets.WebSocketClientProtocol | None = None
20
+ _read_task: asyncio.Task | None = None
21
+
22
+ async def send(self, data: str | bytes) -> None:
23
+ """Send input to the terminal."""
24
+ if self._ws is None:
25
+ raise RuntimeError("PTY session not connected")
26
+ payload = data if isinstance(data, bytes) else data.encode()
27
+ await self._ws.send(payload)
28
+
29
+ async def recv(self) -> bytes:
30
+ """Receive output from the terminal."""
31
+ if self._ws is None:
32
+ raise RuntimeError("PTY session not connected")
33
+ data = await self._ws.recv()
34
+ return data if isinstance(data, bytes) else data.encode()
35
+
36
+ async def close(self) -> None:
37
+ """Close the PTY session."""
38
+ if self._read_task and not self._read_task.done():
39
+ self._read_task.cancel()
40
+ if self._ws:
41
+ await self._ws.close()
42
+
43
+
44
+ @dataclass
45
+ class Pty:
46
+ """PTY terminal session manager for a sandbox."""
47
+
48
+ _client: httpx.AsyncClient
49
+ _sandbox_id: str
50
+ _api_url: str
51
+ _api_key: str
52
+
53
+ async def create(
54
+ self,
55
+ cols: int = 80,
56
+ rows: int = 24,
57
+ on_output: Callable[[bytes], None] | None = None,
58
+ ) -> PtySession:
59
+ """Create a new PTY session and connect via WebSocket."""
60
+ # Create session via REST
61
+ resp = await self._client.post(
62
+ f"/sandboxes/{self._sandbox_id}/pty",
63
+ json={"cols": cols, "rows": rows},
64
+ )
65
+ resp.raise_for_status()
66
+ data = resp.json()
67
+ session_id = data["sessionID"]
68
+
69
+ # Connect via WebSocket
70
+ ws_url = self._api_url.replace("http://", "ws://").replace("https://", "wss://")
71
+ ws_url = f"{ws_url}/sandboxes/{self._sandbox_id}/pty/{session_id}"
72
+
73
+ headers = {}
74
+ if self._api_key:
75
+ headers["X-API-Key"] = self._api_key
76
+
77
+ ws = await websockets.connect(ws_url, additional_headers=headers)
78
+
79
+ session = PtySession(
80
+ session_id=session_id,
81
+ sandbox_id=self._sandbox_id,
82
+ _ws=ws,
83
+ )
84
+
85
+ if on_output:
86
+ async def _reader() -> None:
87
+ try:
88
+ async for msg in ws:
89
+ output = msg if isinstance(msg, bytes) else msg.encode()
90
+ on_output(output)
91
+ except websockets.ConnectionClosed:
92
+ pass
93
+
94
+ session._read_task = asyncio.create_task(_reader())
95
+
96
+ return session
@@ -0,0 +1,201 @@
1
+ """Sandbox class - main entry point for the OpenSandbox SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from opencomputer.commands import Commands
12
+ from opencomputer.filesystem import Filesystem
13
+ from opencomputer.pty import Pty
14
+
15
+
16
+ @dataclass
17
+ class Sandbox:
18
+ """E2B-compatible sandbox interface."""
19
+
20
+ sandbox_id: str
21
+ status: str = "running"
22
+ template: str = ""
23
+ domain: str = ""
24
+ _api_url: str = ""
25
+ _api_key: str = ""
26
+ _connect_url: str = ""
27
+ _token: str = ""
28
+ _client: httpx.AsyncClient = field(default=None, repr=False)
29
+ _data_client: httpx.AsyncClient = field(default=None, repr=False)
30
+
31
+ @classmethod
32
+ async def create(
33
+ cls,
34
+ template: str = "base",
35
+ timeout: int = 300,
36
+ api_key: str | None = None,
37
+ api_url: str | None = None,
38
+ envs: dict[str, str] | None = None,
39
+ metadata: dict[str, str] | None = None,
40
+ ) -> Sandbox:
41
+ """Create a new sandbox instance."""
42
+ url = api_url or os.environ.get("OPENSANDBOX_API_URL", "https://app.opencomputer.dev")
43
+ url = url.rstrip("/")
44
+ key = api_key or os.environ.get("OPENSANDBOX_API_KEY", "")
45
+
46
+ # Control plane client always uses /api prefix
47
+ api_base = url if url.endswith("/api") else f"{url}/api"
48
+
49
+ headers = {}
50
+ if key:
51
+ headers["X-API-Key"] = key
52
+
53
+ client = httpx.AsyncClient(base_url=api_base, headers=headers, timeout=30.0)
54
+
55
+ body: dict[str, Any] = {
56
+ "templateID": template,
57
+ "timeout": timeout,
58
+ }
59
+ if envs:
60
+ body["envs"] = envs
61
+ if metadata:
62
+ body["metadata"] = metadata
63
+
64
+ resp = await client.post("/sandboxes", json=body)
65
+ resp.raise_for_status()
66
+ data = resp.json()
67
+
68
+ connect_url = data.get("connectURL", "")
69
+ token = data.get("token", "")
70
+
71
+ # If worker returned a direct connectURL, create a separate client for data ops
72
+ data_client = None
73
+ if connect_url and token:
74
+ data_client = httpx.AsyncClient(
75
+ base_url=connect_url,
76
+ headers={"Authorization": f"Bearer {token}"},
77
+ timeout=30.0,
78
+ )
79
+
80
+ return cls(
81
+ sandbox_id=data["sandboxID"],
82
+ status=data.get("status", "running"),
83
+ template=template,
84
+ domain=data.get("domain", ""),
85
+ _api_url=url,
86
+ _api_key=key,
87
+ _connect_url=connect_url,
88
+ _token=token,
89
+ _client=client,
90
+ _data_client=data_client,
91
+ )
92
+
93
+ @classmethod
94
+ async def connect(
95
+ cls,
96
+ sandbox_id: str,
97
+ api_key: str | None = None,
98
+ api_url: str | None = None,
99
+ ) -> Sandbox:
100
+ """Connect to an existing sandbox."""
101
+ url = api_url or os.environ.get("OPENSANDBOX_API_URL", "https://app.opencomputer.dev")
102
+ url = url.rstrip("/")
103
+ key = api_key or os.environ.get("OPENSANDBOX_API_KEY", "")
104
+
105
+ api_base = url if url.endswith("/api") else f"{url}/api"
106
+
107
+ headers = {}
108
+ if key:
109
+ headers["X-API-Key"] = key
110
+
111
+ client = httpx.AsyncClient(base_url=api_base, headers=headers, timeout=30.0)
112
+
113
+ resp = await client.get(f"/sandboxes/{sandbox_id}")
114
+ resp.raise_for_status()
115
+ data = resp.json()
116
+
117
+ connect_url = data.get("connectURL", "")
118
+ token = data.get("token", "")
119
+
120
+ data_client = None
121
+ if connect_url and token:
122
+ data_client = httpx.AsyncClient(
123
+ base_url=connect_url,
124
+ headers={"Authorization": f"Bearer {token}"},
125
+ timeout=30.0,
126
+ )
127
+
128
+ return cls(
129
+ sandbox_id=sandbox_id,
130
+ status=data.get("status", "running"),
131
+ template=data.get("templateID", ""),
132
+ domain=data.get("domain", ""),
133
+ _api_url=url,
134
+ _api_key=key,
135
+ _connect_url=connect_url,
136
+ _token=token,
137
+ _client=client,
138
+ _data_client=data_client,
139
+ )
140
+
141
+ @property
142
+ def _ops_client(self) -> httpx.AsyncClient:
143
+ """Return the client for data operations (direct worker if available, else CP)."""
144
+ if self._data_client is not None:
145
+ return self._data_client
146
+ return self._client
147
+
148
+ async def kill(self) -> None:
149
+ """Kill and remove the sandbox."""
150
+ resp = await self._client.delete(f"/sandboxes/{self.sandbox_id}")
151
+ resp.raise_for_status()
152
+ self.status = "stopped"
153
+
154
+ async def is_running(self) -> bool:
155
+ """Check if the sandbox is still running."""
156
+ try:
157
+ resp = await self._client.get(f"/sandboxes/{self.sandbox_id}")
158
+ resp.raise_for_status()
159
+ data = resp.json()
160
+ self.status = data.get("status", "stopped")
161
+ return self.status == "running"
162
+ except httpx.HTTPStatusError:
163
+ return False
164
+
165
+ async def set_timeout(self, timeout: int) -> None:
166
+ """Update the sandbox timeout in seconds."""
167
+ resp = await self._client.post(
168
+ f"/sandboxes/{self.sandbox_id}/timeout",
169
+ json={"timeout": timeout},
170
+ )
171
+ resp.raise_for_status()
172
+
173
+ @property
174
+ def files(self) -> Filesystem:
175
+ """Access filesystem operations."""
176
+ return Filesystem(self._ops_client, self.sandbox_id)
177
+
178
+ @property
179
+ def commands(self) -> Commands:
180
+ """Access command execution."""
181
+ return Commands(self._ops_client, self.sandbox_id)
182
+
183
+ @property
184
+ def pty(self) -> Pty:
185
+ """Access PTY terminal sessions."""
186
+ pty_url = self._connect_url or self._api_url
187
+ pty_key = self._token or self._api_key
188
+ return Pty(self._ops_client, self.sandbox_id, pty_url, pty_key)
189
+
190
+ async def close(self) -> None:
191
+ """Close the HTTP client (does not kill the sandbox)."""
192
+ await self._client.aclose()
193
+ if self._data_client is not None:
194
+ await self._data_client.aclose()
195
+
196
+ async def __aenter__(self) -> Sandbox:
197
+ return self
198
+
199
+ async def __aexit__(self, *args: object) -> None:
200
+ await self.kill()
201
+ await self.close()
@@ -0,0 +1,75 @@
1
+ """Template management for custom sandbox environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import httpx
8
+
9
+
10
+ @dataclass
11
+ class TemplateInfo:
12
+ """Template metadata."""
13
+
14
+ template_id: str
15
+ name: str
16
+ tag: str
17
+ status: str
18
+
19
+
20
+ @dataclass
21
+ class Template:
22
+ """Template management operations."""
23
+
24
+ _client: httpx.AsyncClient
25
+
26
+ @classmethod
27
+ def _from_client(cls, client: httpx.AsyncClient) -> Template:
28
+ return cls(_client=client)
29
+
30
+ async def build(self, name: str, dockerfile: str) -> TemplateInfo:
31
+ """Build a new template from a Dockerfile."""
32
+ resp = await self._client.post(
33
+ "/templates",
34
+ json={"name": name, "dockerfile": dockerfile},
35
+ timeout=300.0,
36
+ )
37
+ resp.raise_for_status()
38
+ data = resp.json()
39
+ return TemplateInfo(
40
+ template_id=data["templateID"],
41
+ name=data["name"],
42
+ tag=data.get("tag", "latest"),
43
+ status=data.get("status", "ready"),
44
+ )
45
+
46
+ async def list(self) -> list[TemplateInfo]:
47
+ """List all available templates."""
48
+ resp = await self._client.get("/templates")
49
+ resp.raise_for_status()
50
+ return [
51
+ TemplateInfo(
52
+ template_id=t["templateID"],
53
+ name=t["name"],
54
+ tag=t.get("tag", "latest"),
55
+ status=t.get("status", "ready"),
56
+ )
57
+ for t in resp.json()
58
+ ]
59
+
60
+ async def get(self, name: str) -> TemplateInfo:
61
+ """Get template details by name."""
62
+ resp = await self._client.get(f"/templates/{name}")
63
+ resp.raise_for_status()
64
+ data = resp.json()
65
+ return TemplateInfo(
66
+ template_id=data["templateID"],
67
+ name=data["name"],
68
+ tag=data.get("tag", "latest"),
69
+ status=data.get("status", "ready"),
70
+ )
71
+
72
+ async def delete(self, name: str) -> None:
73
+ """Delete a template."""
74
+ resp = await self._client.delete(f"/templates/{name}")
75
+ resp.raise_for_status()
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "opencomputer-sdk"
7
+ version = "0.3.0"
8
+ description = "Python SDK for OpenComputer - cloud sandbox platform"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "OpenComputer" },
14
+ ]
15
+ keywords = ["sandbox", "opencomputer", "cloud", "containers", "code-execution"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.27.0",
29
+ "websockets>=12.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=8.0",
35
+ "pytest-asyncio>=0.23",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/diggerhq/opensandbox"
40
+ Repository = "https://github.com/diggerhq/opensandbox"
41
+ Documentation = "https://github.com/diggerhq/opensandbox/tree/main/sdks/python"
42
+ Issues = "https://github.com/diggerhq/opensandbox/issues"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["opencomputer"]
46
+ exclude = ["__pycache__"]