opencomputer-sdk 0.6.1__tar.gz → 0.6.3__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 (38) hide show
  1. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/PKG-INFO +1 -1
  2. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/__init__.py +1 -1
  3. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/image.py +35 -20
  4. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/sandbox.py +45 -0
  5. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/pyproject.toml +1 -1
  6. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/.gitignore +0 -0
  7. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/README.md +0 -0
  8. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/run_all_tests.py +0 -0
  9. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/stream_demo.py +0 -0
  10. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_commands.py +0 -0
  11. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_concurrent.py +0 -0
  12. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_declarative_images.py +0 -0
  13. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_default_template.py +0 -0
  14. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_disk_size.py +0 -0
  15. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_domain_tls.py +0 -0
  16. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_environment.py +0 -0
  17. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_exec.py +0 -0
  18. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_file_ops.py +0 -0
  19. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_large_files.py +0 -0
  20. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_multi_template.py +0 -0
  21. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_python_sdk.py +0 -0
  22. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_reconnect.py +0 -0
  23. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_secret_store_fork.py +0 -0
  24. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_secretstore.py +0 -0
  25. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_shell.py +0 -0
  26. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_timeout.py +0 -0
  27. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/agent.py +0 -0
  28. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/commands.py +0 -0
  29. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/exec.py +0 -0
  30. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/filesystem.py +0 -0
  31. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/mounts.py +0 -0
  32. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/project.py +0 -0
  33. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/pty.py +0 -0
  34. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/shell.py +0 -0
  35. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/snapshot.py +0 -0
  36. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/sse.py +0 -0
  37. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/template.py +0 -0
  38. {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencomputer-sdk
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: Python SDK for OpenComputer - cloud sandbox platform
5
5
  Project-URL: Homepage, https://github.com/diggerhq/opensandbox
6
6
  Project-URL: Repository, https://github.com/diggerhq/opensandbox
@@ -65,4 +65,4 @@ __all__ = [
65
65
  "TagKeyInfo",
66
66
  ]
67
67
 
68
- __version__ = "0.6.0"
68
+ __version__ = "0.6.3"
@@ -6,7 +6,7 @@ import base64
6
6
  import hashlib
7
7
  import json
8
8
  import os
9
- from dataclasses import dataclass, field
9
+ from dataclasses import dataclass, field, replace
10
10
  from typing import Any
11
11
 
12
12
 
@@ -47,6 +47,10 @@ class Image:
47
47
 
48
48
  _base: str = "base"
49
49
  _steps: tuple[ImageStep, ...] = field(default_factory=tuple)
50
+ # RAM for the build phase (apt/pip). 0 = server default. Does not pin the
51
+ # output image — the server re-snapshots at the default 1 GB floor, and you
52
+ # size the actual sandbox at create time via memory_mb.
53
+ _builder_memory_mb: int = 0
50
54
 
51
55
  @classmethod
52
56
  def base(cls) -> Image:
@@ -60,36 +64,36 @@ class Image:
60
64
 
61
65
  def apt_install(self, packages: list[str]) -> Image:
62
66
  """Install system packages via apt-get."""
63
- return Image(
64
- _base=self._base,
67
+ return replace(
68
+ self,
65
69
  _steps=(*self._steps, ImageStep("apt_install", {"packages": packages})),
66
70
  )
67
71
 
68
72
  def pip_install(self, packages: list[str]) -> Image:
69
73
  """Install Python packages via pip."""
70
- return Image(
71
- _base=self._base,
74
+ return replace(
75
+ self,
72
76
  _steps=(*self._steps, ImageStep("pip_install", {"packages": packages})),
73
77
  )
74
78
 
75
79
  def run_commands(self, *commands: str) -> Image:
76
80
  """Run one or more shell commands."""
77
- return Image(
78
- _base=self._base,
81
+ return replace(
82
+ self,
79
83
  _steps=(*self._steps, ImageStep("run", {"commands": list(commands)})),
80
84
  )
81
85
 
82
86
  def env(self, vars: dict[str, str]) -> Image:
83
87
  """Set environment variables (written to /etc/environment)."""
84
- return Image(
85
- _base=self._base,
88
+ return replace(
89
+ self,
86
90
  _steps=(*self._steps, ImageStep("env", {"vars": vars})),
87
91
  )
88
92
 
89
93
  def workdir(self, path: str) -> Image:
90
94
  """Set the default working directory."""
91
- return Image(
92
- _base=self._base,
95
+ return replace(
96
+ self,
93
97
  _steps=(*self._steps, ImageStep("workdir", {"path": path})),
94
98
  )
95
99
 
@@ -101,8 +105,8 @@ class Image:
101
105
  content: String content of the file.
102
106
  """
103
107
  encoded = base64.b64encode(content.encode()).decode()
104
- return Image(
105
- _base=self._base,
108
+ return replace(
109
+ self,
106
110
  _steps=(*self._steps, ImageStep("add_file", {
107
111
  "path": remote_path,
108
112
  "content": encoded,
@@ -121,8 +125,8 @@ class Image:
121
125
  """
122
126
  with open(local_path, "rb") as f:
123
127
  encoded = base64.b64encode(f.read()).decode()
124
- return Image(
125
- _base=self._base,
128
+ return replace(
129
+ self,
126
130
  _steps=(*self._steps, ImageStep("add_file", {
127
131
  "path": remote_path,
128
132
  "content": encoded,
@@ -147,22 +151,33 @@ class Image:
147
151
  with open(full, "rb") as f:
148
152
  encoded = base64.b64encode(f.read()).decode()
149
153
  files.append({"relativePath": rel, "content": encoded})
150
- return Image(
151
- _base=self._base,
154
+ return replace(
155
+ self,
152
156
  _steps=(*self._steps, ImageStep("add_dir", {
153
157
  "path": remote_path,
154
158
  "files": files,
155
159
  })),
156
160
  )
157
161
 
162
+ def builder_memory(self, mb: int) -> Image:
163
+ """Set the RAM (MB) for the build phase. Use this when a build OOMs at the
164
+ default 4 GB (e.g. heavy ``apt``/``pip``/``npm``). Does not affect the
165
+ resulting image's memory — size the sandbox at create time via ``memory_mb``."""
166
+ return replace(self, _builder_memory_mb=mb)
167
+
158
168
  def to_dict(self) -> dict[str, Any]:
159
169
  """Returns the manifest as a plain dict (for JSON serialization)."""
160
- return {
170
+ d: dict[str, Any] = {
161
171
  "base": self._base,
162
172
  "steps": [s.to_dict() for s in self._steps],
163
173
  }
174
+ if self._builder_memory_mb > 0:
175
+ d["builderMemoryMB"] = self._builder_memory_mb
176
+ return d
164
177
 
165
178
  def cache_key(self) -> str:
166
- """Compute a deterministic content hash for caching."""
167
- canonical = json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":"))
179
+ """Deterministic content hash for caching. Memory knobs are resource
180
+ params, not image content, so they're excluded (matches the server)."""
181
+ content = {"base": self._base, "steps": [s.to_dict() for s in self._steps]}
182
+ canonical = json.dumps(content, sort_keys=True, separators=(",", ":"))
168
183
  return hashlib.sha256(canonical.encode()).hexdigest()
@@ -70,6 +70,12 @@ class Sandbox:
70
70
  sandbox_id: str
71
71
  status: str = "running"
72
72
  template: str = ""
73
+ #: Plaintext preview-URL bearer token, available immediately after a
74
+ #: ``Sandbox.create(preview_auth=...)`` call. Read it once and store
75
+ #: it somewhere durable — the server will not return it again. After
76
+ #: a successful ``rotate_preview_auth_token()`` this value is replaced
77
+ #: with the new token. Empty string when not enabled or when reconnecting.
78
+ preview_auth_token: str = ""
73
79
  _api_url: str = ""
74
80
  _api_key: str = ""
75
81
  _connect_url: str = ""
@@ -77,6 +83,10 @@ class Sandbox:
77
83
  _client: httpx.AsyncClient = field(default=None, repr=False)
78
84
  _data_client: httpx.AsyncClient = field(default=None, repr=False)
79
85
 
86
+ @property
87
+ def id(self) -> str:
88
+ return self.sandbox_id
89
+
80
90
  @classmethod
81
91
  async def create(
82
92
  cls,
@@ -94,6 +104,7 @@ class Sandbox:
94
104
  image: Image | None = None,
95
105
  snapshot: str | None = None,
96
106
  on_build_log: Callable[[str], None] | None = None,
107
+ preview_auth: dict[str, str] | None = None,
97
108
  ) -> Sandbox:
98
109
  """Create a new sandbox instance.
99
110
 
@@ -121,6 +132,15 @@ class Sandbox:
121
132
  image: Declarative Image definition. The server builds and caches it as a checkpoint.
122
133
  snapshot: Name of a pre-built snapshot to create the sandbox from.
123
134
  on_build_log: Callback for build log streaming when using image/snapshot.
135
+ preview_auth: Require a bearer token on the sandbox's preview URLs.
136
+ When set, every request to ``https://sb-{id}-p{port}.<domain>``
137
+ must include the token in an ``Authorization: Bearer <token>``
138
+ or ``X-OC-Preview-Token`` header. The check happens at the edge
139
+ before traffic reaches the VM. Pass ``{"token": "auto"}`` (or
140
+ omit the key) to have the server generate a 256-bit random
141
+ token; pass an explicit string (>=16 chars) to bring your own.
142
+ The plaintext is returned exactly once and assigned to
143
+ ``sandbox.preview_auth_token``.
124
144
  """
125
145
  url = api_url or os.environ.get("OPENCOMPUTER_API_URL", "https://app.opencomputer.dev")
126
146
  url = url.rstrip("/")
@@ -165,6 +185,11 @@ class Sandbox:
165
185
  body["image"] = image.to_dict()
166
186
  if snapshot is not None:
167
187
  body["snapshot"] = snapshot
188
+ if preview_auth is not None:
189
+ body["previewAuth"] = {
190
+ "scheme": preview_auth.get("scheme", "bearer"),
191
+ "token": preview_auth.get("token", "auto"),
192
+ }
168
193
 
169
194
  if use_sse:
170
195
  data = await cls._create_with_sse(client, body, on_build_log)
@@ -189,6 +214,7 @@ class Sandbox:
189
214
  sandbox_id=data["sandboxID"],
190
215
  status=data.get("status", "running"),
191
216
  template=template,
217
+ preview_auth_token=data.get("previewAuthToken", ""),
192
218
  _api_url=url,
193
219
  _api_key=key,
194
220
  _connect_url=connect_url,
@@ -809,6 +835,25 @@ class Sandbox:
809
835
  if resp.status_code != 404:
810
836
  resp.raise_for_status()
811
837
 
838
+ async def rotate_preview_auth_token(self) -> str:
839
+ """Issue a new preview-URL bearer token; invalidate the previous one.
840
+
841
+ The old token stops working immediately — there is no zero-downtime
842
+ dual-token mode in v1, so coordinate the rollover with whoever is
843
+ calling your preview URL. If the sandbox was created without
844
+ ``preview_auth``, calling this enables the auth gate from now on.
845
+
846
+ Returns the new plaintext token (also written to
847
+ ``self.preview_auth_token``).
848
+ """
849
+ resp = await self._client.post(
850
+ f"/sandboxes/{self.sandbox_id}/preview/rotate",
851
+ )
852
+ resp.raise_for_status()
853
+ data = resp.json()
854
+ self.preview_auth_token = data["previewAuthToken"]
855
+ return self.preview_auth_token
856
+
812
857
  async def create_preview_url(self, port: int, domain: str | None = None, auth_config: dict | None = None) -> dict:
813
858
  """Create a preview URL targeting a specific container port.
814
859
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opencomputer-sdk"
7
- version = "0.6.1"
7
+ version = "0.6.3"
8
8
  description = "Python SDK for OpenComputer - cloud sandbox platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"