daimon-sdk 0.4.2__tar.gz → 0.4.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.
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/PKG-INFO +5 -1
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/README.md +4 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/pyproject.toml +1 -1
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/src/daimon_sdk/manager.py +12 -3
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/tests/test_e2e.py +28 -1
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/tests/test_unit.py +35 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/.github/workflows/publish.yml +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/.gitignore +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/examples/auth.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/examples/exec_session.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/examples/files.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/examples/manager.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/examples/runtime_and_web.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/src/daimon_sdk/__init__.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/src/daimon_sdk/_transport.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/src/daimon_sdk/client.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/src/daimon_sdk/exceptions.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/src/daimon_sdk/models.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/tests/conftest.py +0 -0
- {daimon_sdk-0.4.2 → daimon_sdk-0.4.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: daimon-sdk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Typed async Python SDK for daimon MCP services.
|
|
5
5
|
Author: processd contributors
|
|
6
6
|
License: MIT
|
|
@@ -116,12 +116,16 @@ print(read.file.content)
|
|
|
116
116
|
- `await manager.find_or_create_sandbox(labels={"thread_id": thread_id})`
|
|
117
117
|
- `await manager.get_sandbox(id)`
|
|
118
118
|
- `await manager.start_sandbox(id) / stop_sandbox(id) / delete_sandbox(id)`
|
|
119
|
+
- `await manager.update_sandbox(id, ttl_seconds=...)`
|
|
119
120
|
- `async with manager.sandbox() as sandbox`
|
|
121
|
+
- `await sandbox.set_ttl(ttl_seconds)`
|
|
120
122
|
- `sandbox.runtime/files/exec/web/raw`
|
|
121
123
|
|
|
122
124
|
`manager.sandbox()` creates a sandbox, connects to its MCP endpoint, and deletes
|
|
123
125
|
it on context exit by default. Use `delete_on_exit=False` or
|
|
124
126
|
`create_sandbox()` when the workspace should survive beyond the context.
|
|
127
|
+
`ttl_seconds=0` marks a sandbox as immediately expired so the next manager
|
|
128
|
+
reaper loop deletes it.
|
|
125
129
|
|
|
126
130
|
## Local Testing
|
|
127
131
|
|
|
@@ -102,12 +102,16 @@ print(read.file.content)
|
|
|
102
102
|
- `await manager.find_or_create_sandbox(labels={"thread_id": thread_id})`
|
|
103
103
|
- `await manager.get_sandbox(id)`
|
|
104
104
|
- `await manager.start_sandbox(id) / stop_sandbox(id) / delete_sandbox(id)`
|
|
105
|
+
- `await manager.update_sandbox(id, ttl_seconds=...)`
|
|
105
106
|
- `async with manager.sandbox() as sandbox`
|
|
107
|
+
- `await sandbox.set_ttl(ttl_seconds)`
|
|
106
108
|
- `sandbox.runtime/files/exec/web/raw`
|
|
107
109
|
|
|
108
110
|
`manager.sandbox()` creates a sandbox, connects to its MCP endpoint, and deletes
|
|
109
111
|
it on context exit by default. Use `delete_on_exit=False` or
|
|
110
112
|
`create_sandbox()` when the workspace should survive beyond the context.
|
|
113
|
+
`ttl_seconds=0` marks a sandbox as immediately expired so the next manager
|
|
114
|
+
reaper loop deletes it.
|
|
111
115
|
|
|
112
116
|
## Local Testing
|
|
113
117
|
|
|
@@ -119,7 +119,7 @@ class DaimonSandbox:
|
|
|
119
119
|
async def close(self) -> None:
|
|
120
120
|
await self.client.close()
|
|
121
121
|
|
|
122
|
-
async def set_ttl(self, ttl_seconds: int
|
|
122
|
+
async def set_ttl(self, ttl_seconds: int) -> SandboxInfo:
|
|
123
123
|
self.info = await self._manager.update_sandbox(self.id, ttl_seconds=ttl_seconds)
|
|
124
124
|
return self.info
|
|
125
125
|
|
|
@@ -257,9 +257,18 @@ class DaimonManagerClient:
|
|
|
257
257
|
async def delete_sandbox(self, sandbox_id: str) -> None:
|
|
258
258
|
await self._transport.request("DELETE", f"/sandboxes/{sandbox_id}")
|
|
259
259
|
|
|
260
|
-
async def update_sandbox(
|
|
260
|
+
async def update_sandbox(
|
|
261
|
+
self,
|
|
262
|
+
sandbox_id: str,
|
|
263
|
+
*,
|
|
264
|
+
ttl_seconds: int | None = None,
|
|
265
|
+
**updates: Any,
|
|
266
|
+
) -> SandboxInfo:
|
|
267
|
+
body = {key: value for key, value in updates.items() if value is not None}
|
|
268
|
+
if ttl_seconds is not None:
|
|
269
|
+
body["ttl_seconds"] = ttl_seconds
|
|
261
270
|
return SandboxInfo.from_dict(
|
|
262
|
-
await self._transport.json("PATCH", f"/sandboxes/{sandbox_id}", body=
|
|
271
|
+
await self._transport.json("PATCH", f"/sandboxes/{sandbox_id}", body=body or None)
|
|
263
272
|
)
|
|
264
273
|
|
|
265
274
|
def sandbox(self, *, delete_on_exit: bool = True) -> SandboxContext:
|
|
@@ -13,7 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
import httpx
|
|
14
14
|
import pytest
|
|
15
15
|
|
|
16
|
-
from daimon_sdk import DaimonHttpError, DaimonManagerClient, DaimonToolError
|
|
16
|
+
from daimon_sdk import DaimonConnectionError, DaimonHttpError, DaimonManagerClient, DaimonToolError
|
|
17
17
|
|
|
18
18
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
19
19
|
PROCESSD_ROOT = REPO_ROOT.parent / "processd-standalone"
|
|
@@ -219,8 +219,35 @@ async def test_background_bash_and_auth(auth_client) -> None:
|
|
|
219
219
|
if "[process exited with code 0]" in final_text:
|
|
220
220
|
break
|
|
221
221
|
await asyncio.sleep(0.1)
|
|
222
|
+
|
|
222
223
|
assert final_text is not None
|
|
223
224
|
assert "start" in final_text
|
|
225
|
+
assert "end" in final_text
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@pytest.mark.asyncio
|
|
229
|
+
@pytest.mark.skipif(not MANAGER_E2E_ENABLED, reason="set PROCESSD_SDK_MANAGER_E2E=1")
|
|
230
|
+
async def test_docker_manager_ttl_zero_expires_on_next_reaper_loop() -> None:
|
|
231
|
+
with _start_docker_manager(
|
|
232
|
+
extra_env={"PROCESSD_MANAGER_REAPER_INTERVAL_SECONDS": "1"}
|
|
233
|
+
) as manager_url:
|
|
234
|
+
async with DaimonManagerClient(manager_url) as manager:
|
|
235
|
+
sandbox = await manager.create_sandbox()
|
|
236
|
+
updated = await sandbox.set_ttl(0)
|
|
237
|
+
assert updated.ttl_seconds == 0
|
|
238
|
+
|
|
239
|
+
deadline = time.monotonic() + 10
|
|
240
|
+
while time.monotonic() < deadline:
|
|
241
|
+
try:
|
|
242
|
+
await manager.get_sandbox(sandbox.id)
|
|
243
|
+
except DaimonHttpError as exc:
|
|
244
|
+
assert exc.status_code == 404
|
|
245
|
+
break
|
|
246
|
+
except DaimonConnectionError:
|
|
247
|
+
pass
|
|
248
|
+
await asyncio.sleep(1)
|
|
249
|
+
else:
|
|
250
|
+
raise AssertionError("sandbox was not reaped after ttl_seconds=0")
|
|
224
251
|
|
|
225
252
|
|
|
226
253
|
@pytest.mark.manager_e2e
|
|
@@ -253,6 +253,13 @@ async def test_daimon_sandbox_lifecycle_updates_info_and_closes_client() -> None
|
|
|
253
253
|
async def delete_sandbox(self, sandbox_id: str) -> None:
|
|
254
254
|
self.deleted = sandbox_id
|
|
255
255
|
|
|
256
|
+
async def update_sandbox(self, sandbox_id: str, *, ttl_seconds: int | None = None, **updates) -> SandboxInfo:
|
|
257
|
+
payload = dict(SANDBOX_PAYLOAD)
|
|
258
|
+
payload["id"] = sandbox_id
|
|
259
|
+
payload["ttl_seconds"] = ttl_seconds
|
|
260
|
+
payload["expires_at"] = payload["last_used_at"] + ttl_seconds if ttl_seconds is not None else payload["expires_at"]
|
|
261
|
+
return SandboxInfo.from_dict(payload)
|
|
262
|
+
|
|
256
263
|
manager = DummyManager()
|
|
257
264
|
sandbox = DaimonSandbox(manager, SandboxInfo.from_dict(SANDBOX_PAYLOAD), timeout_s=30)
|
|
258
265
|
first_client = DummyClient()
|
|
@@ -273,6 +280,34 @@ async def test_daimon_sandbox_lifecycle_updates_info_and_closes_client() -> None
|
|
|
273
280
|
assert replacement_client.closed == 1
|
|
274
281
|
assert sandbox.info.state == "deleted"
|
|
275
282
|
|
|
283
|
+
updated = await sandbox.set_ttl(0)
|
|
284
|
+
assert updated.ttl_seconds == 0
|
|
285
|
+
assert sandbox.info.ttl_seconds == 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_manager_update_sandbox_omits_none_fields(monkeypatch) -> None:
|
|
290
|
+
from daimon_sdk.manager import DaimonManagerClient
|
|
291
|
+
|
|
292
|
+
captured: dict[str, object] = {}
|
|
293
|
+
|
|
294
|
+
async def fake_json(method: str, path: str, *, body=None):
|
|
295
|
+
captured["method"] = method
|
|
296
|
+
captured["path"] = path
|
|
297
|
+
captured["body"] = body
|
|
298
|
+
return dict(SANDBOX_PAYLOAD)
|
|
299
|
+
|
|
300
|
+
manager = DaimonManagerClient("http://127.0.0.1:18080")
|
|
301
|
+
monkeypatch.setattr(manager._transport, "json", fake_json)
|
|
302
|
+
|
|
303
|
+
await manager.update_sandbox("sandbox-1", ttl_seconds=0, note=None)
|
|
304
|
+
|
|
305
|
+
assert captured == {
|
|
306
|
+
"method": "PATCH",
|
|
307
|
+
"path": "/sandboxes/sandbox-1",
|
|
308
|
+
"body": {"ttl_seconds": 0},
|
|
309
|
+
}
|
|
310
|
+
|
|
276
311
|
|
|
277
312
|
@pytest.mark.asyncio
|
|
278
313
|
async def test_manager_sandbox_context_deletes_on_exception(monkeypatch) -> None:
|
|
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
|
|
File without changes
|