runcloud-sdk 0.8.3__tar.gz → 0.10.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.
- {runcloud_sdk-0.8.3/runcloud_sdk.egg-info → runcloud_sdk-0.10.0}/PKG-INFO +16 -1
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/README.md +15 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/pyproject.toml +1 -1
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/client.py +52 -2
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0/runcloud_sdk.egg-info}/PKG-INFO +16 -1
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/tests/test_sdk.py +58 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/LICENSE +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/__init__.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/__init__.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/blaxel.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/daytona.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/e2b.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/modal.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/sprites.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/vercel.py +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/SOURCES.txt +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/dependency_links.txt +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/requires.txt +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/top_level.txt +0 -0
- {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runcloud-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Python SDK for run.cloud, including sandbox-provider compatibility adapters
|
|
5
5
|
Author: Newly
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -62,6 +62,21 @@ finally:
|
|
|
62
62
|
automation, set `RUN_CLOUD_API_KEY` and optionally `RUN_CLOUD_API_URL`, or pass
|
|
63
63
|
`api_key` and `api_url` directly.
|
|
64
64
|
|
|
65
|
+
A sandbox's name is a label you own: set it at create, change it later, and
|
|
66
|
+
filter on it. Naming plus `resume` is how the same sandbox goes back to the same
|
|
67
|
+
piece of work instead of booting a new one:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
matches = cloud.list(name=f"project-{project_id}")
|
|
71
|
+
box = matches[0].resume() if matches else cloud.create(name=f"project-{project_id}")
|
|
72
|
+
|
|
73
|
+
box.rename(f"project-{project_id}-v2")
|
|
74
|
+
box.rename("") # clears the label
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Names are not unique and mean nothing to the platform, so check `state` on a
|
|
78
|
+
match before using it. Renaming works in any state but `destroyed`.
|
|
79
|
+
|
|
65
80
|
## Compatibility adapters
|
|
66
81
|
|
|
67
82
|
The same distribution includes migration adapters:
|
|
@@ -44,6 +44,21 @@ finally:
|
|
|
44
44
|
automation, set `RUN_CLOUD_API_KEY` and optionally `RUN_CLOUD_API_URL`, or pass
|
|
45
45
|
`api_key` and `api_url` directly.
|
|
46
46
|
|
|
47
|
+
A sandbox's name is a label you own: set it at create, change it later, and
|
|
48
|
+
filter on it. Naming plus `resume` is how the same sandbox goes back to the same
|
|
49
|
+
piece of work instead of booting a new one:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
matches = cloud.list(name=f"project-{project_id}")
|
|
53
|
+
box = matches[0].resume() if matches else cloud.create(name=f"project-{project_id}")
|
|
54
|
+
|
|
55
|
+
box.rename(f"project-{project_id}-v2")
|
|
56
|
+
box.rename("") # clears the label
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Names are not unique and mean nothing to the platform, so check `state` on a
|
|
60
|
+
match before using it. Renaming works in any state but `destroyed`.
|
|
61
|
+
|
|
47
62
|
## Compatibility adapters
|
|
48
63
|
|
|
49
64
|
The same distribution includes migration adapters:
|
|
@@ -354,6 +354,7 @@ class Client:
|
|
|
354
354
|
timeout_seconds: Optional[int] = None,
|
|
355
355
|
region: Optional[str] = None,
|
|
356
356
|
name: Optional[str] = None,
|
|
357
|
+
tags: Optional[dict[str, str]] = None,
|
|
357
358
|
) -> "Sandbox":
|
|
358
359
|
values = {
|
|
359
360
|
"image": image,
|
|
@@ -364,6 +365,7 @@ class Client:
|
|
|
364
365
|
"timeoutSeconds": timeout_seconds,
|
|
365
366
|
"region": region,
|
|
366
367
|
"name": name,
|
|
368
|
+
"tags": tags,
|
|
367
369
|
}
|
|
368
370
|
body = {key: value for key, value in values.items() if value is not None}
|
|
369
371
|
return Sandbox(self, self.request("POST", "/run-cloud/sandboxes", body))
|
|
@@ -372,8 +374,22 @@ class Client:
|
|
|
372
374
|
sid = urllib.parse.quote(sandbox_id, safe="")
|
|
373
375
|
return Sandbox(self, self.request("GET", f"/run-cloud/sandboxes/{sid}"))
|
|
374
376
|
|
|
375
|
-
def list(
|
|
376
|
-
|
|
377
|
+
def list(
|
|
378
|
+
self,
|
|
379
|
+
state: Optional[str] = None,
|
|
380
|
+
name: Optional[str] = None,
|
|
381
|
+
tags: Optional[dict[str, str]] = None,
|
|
382
|
+
) -> list["Sandbox"]:
|
|
383
|
+
# Every filter is applied server-side, so finding a sandbox by the name
|
|
384
|
+
# you gave it does not mean paging the whole fleet and matching locally.
|
|
385
|
+
# Repeated `tag` params are ANDed together.
|
|
386
|
+
filters = [
|
|
387
|
+
(key, value)
|
|
388
|
+
for key, value in (("state", state), ("name", name))
|
|
389
|
+
if value
|
|
390
|
+
]
|
|
391
|
+
filters.extend(("tag", f"{key}:{value}") for key, value in (tags or {}).items())
|
|
392
|
+
query = "?" + urllib.parse.urlencode(filters) if filters else ""
|
|
377
393
|
payload = self.request("GET", "/run-cloud/sandboxes" + query) or {}
|
|
378
394
|
return [Sandbox(self, item) for item in payload.get("items", [])]
|
|
379
395
|
|
|
@@ -392,6 +408,25 @@ class Sandbox:
|
|
|
392
408
|
def name(self) -> str:
|
|
393
409
|
return str(self._data.get("name") or "")
|
|
394
410
|
|
|
411
|
+
@property
|
|
412
|
+
def tags(self) -> dict[str, str]:
|
|
413
|
+
value = self._data.get("tags")
|
|
414
|
+
return dict(value) if isinstance(value, dict) else {}
|
|
415
|
+
|
|
416
|
+
def set_tags(self, tags: dict[str, Optional[str]]) -> "Sandbox":
|
|
417
|
+
"""Merge tags into this sandbox. A None value deletes that key.
|
|
418
|
+
|
|
419
|
+
Tags survive the sandbox's destruction, so they stay usable for
|
|
420
|
+
attribution and support long after the sandbox is gone.
|
|
421
|
+
"""
|
|
422
|
+
sid = urllib.parse.quote(self.id, safe="")
|
|
423
|
+
updated = self._client.request(
|
|
424
|
+
"PATCH", f"/run-cloud/sandboxes/{sid}/tags", {"tags": tags}
|
|
425
|
+
)
|
|
426
|
+
if isinstance(updated, dict) and isinstance(updated.get("tags"), dict):
|
|
427
|
+
self._data["tags"] = updated["tags"]
|
|
428
|
+
return self
|
|
429
|
+
|
|
395
430
|
def refresh(self) -> "Sandbox":
|
|
396
431
|
self._data = self._client.request(
|
|
397
432
|
"GET", f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}"
|
|
@@ -548,6 +583,21 @@ class Sandbox:
|
|
|
548
583
|
)
|
|
549
584
|
return self
|
|
550
585
|
|
|
586
|
+
def rename(self, name: str) -> "Sandbox":
|
|
587
|
+
"""Relabel the sandbox.
|
|
588
|
+
|
|
589
|
+
The name is a label you own and the filter ``list(name=...)`` matches on,
|
|
590
|
+
so this is how a sandbox gets re-tagged without destroying it. Works in
|
|
591
|
+
any state but destroyed; an empty name clears the label. Names are not
|
|
592
|
+
unique and are capped at 128 characters.
|
|
593
|
+
"""
|
|
594
|
+
self._data = self._client.request(
|
|
595
|
+
"POST",
|
|
596
|
+
f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}/name",
|
|
597
|
+
{"name": name},
|
|
598
|
+
)
|
|
599
|
+
return self
|
|
600
|
+
|
|
551
601
|
def set_timeout(self, seconds: int) -> "Sandbox":
|
|
552
602
|
self._data = self._client.request(
|
|
553
603
|
"POST",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runcloud-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Python SDK for run.cloud, including sandbox-provider compatibility adapters
|
|
5
5
|
Author: Newly
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -62,6 +62,21 @@ finally:
|
|
|
62
62
|
automation, set `RUN_CLOUD_API_KEY` and optionally `RUN_CLOUD_API_URL`, or pass
|
|
63
63
|
`api_key` and `api_url` directly.
|
|
64
64
|
|
|
65
|
+
A sandbox's name is a label you own: set it at create, change it later, and
|
|
66
|
+
filter on it. Naming plus `resume` is how the same sandbox goes back to the same
|
|
67
|
+
piece of work instead of booting a new one:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
matches = cloud.list(name=f"project-{project_id}")
|
|
71
|
+
box = matches[0].resume() if matches else cloud.create(name=f"project-{project_id}")
|
|
72
|
+
|
|
73
|
+
box.rename(f"project-{project_id}-v2")
|
|
74
|
+
box.rename("") # clears the label
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Names are not unique and mean nothing to the platform, so check `state` on a
|
|
78
|
+
match before using it. Renaming works in any state but `destroyed`.
|
|
79
|
+
|
|
65
80
|
## Compatibility adapters
|
|
66
81
|
|
|
67
82
|
The same distribution includes migration adapters:
|
|
@@ -114,6 +114,35 @@ class SDKTests(unittest.TestCase):
|
|
|
114
114
|
self.recorder.calls,
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
+
def test_rename_posts_the_label_and_list_filters_on_it(self):
|
|
118
|
+
box = self.client.create()
|
|
119
|
+
box.rename("proj_42")
|
|
120
|
+
self.assertIn(
|
|
121
|
+
("POST", "/run-cloud/sandboxes/sb_python/name", {"name": "proj_42"}),
|
|
122
|
+
self.recorder.calls,
|
|
123
|
+
)
|
|
124
|
+
# The response replaces the cached record, so the new name is readable
|
|
125
|
+
# without a second get().
|
|
126
|
+
self.assertEqual(box.name, "compat")
|
|
127
|
+
|
|
128
|
+
# An empty name clears the label rather than being rejected client-side.
|
|
129
|
+
box.rename("")
|
|
130
|
+
self.assertIn(
|
|
131
|
+
("POST", "/run-cloud/sandboxes/sb_python/name", {"name": ""}),
|
|
132
|
+
self.recorder.calls,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.client.list(name="proj_42")
|
|
136
|
+
self.assertIn(
|
|
137
|
+
("GET", "/run-cloud/sandboxes?name=proj_42", None),
|
|
138
|
+
self.recorder.calls,
|
|
139
|
+
)
|
|
140
|
+
self.client.list(state="running", name="proj 42")
|
|
141
|
+
self.assertIn(
|
|
142
|
+
("GET", "/run-cloud/sandboxes?state=running&name=proj+42", None),
|
|
143
|
+
self.recorder.calls,
|
|
144
|
+
)
|
|
145
|
+
|
|
117
146
|
def test_exec_contract_defaults_legacy_true_and_preserves_false(self):
|
|
118
147
|
for frame, expected in [
|
|
119
148
|
({"stream": "exit"}, 0),
|
|
@@ -387,6 +416,35 @@ class SDKTests(unittest.TestCase):
|
|
|
387
416
|
self.assertIsNone(delete_request.get_header("Content-type"))
|
|
388
417
|
self.assertEqual(post_request.get_header("Content-type"), "application/json")
|
|
389
418
|
|
|
419
|
+
def test_tags_are_sent_on_create_and_exposed_on_the_record(self):
|
|
420
|
+
box = self.client.create(tags={"newly.session": "sess-1"})
|
|
421
|
+
method, path, body = self.recorder.calls[0]
|
|
422
|
+
self.assertEqual((method, path), ("POST", "/run-cloud/sandboxes"))
|
|
423
|
+
self.assertEqual(body["tags"], {"newly.session": "sess-1"})
|
|
424
|
+
# The fixture carries no tags, so an untagged payload must read as {}.
|
|
425
|
+
self.assertEqual(box.tags, {})
|
|
426
|
+
|
|
427
|
+
def test_create_omits_tags_when_none_are_given(self):
|
|
428
|
+
self.client.create()
|
|
429
|
+
_, _, body = self.recorder.calls[0]
|
|
430
|
+
self.assertNotIn("tags", body)
|
|
431
|
+
|
|
432
|
+
def test_list_ands_repeated_tag_filters(self):
|
|
433
|
+
self.client.list(state="running", tags={"newly.role": "agent", "env": "dev"})
|
|
434
|
+
_, path, _ = self.recorder.calls[0]
|
|
435
|
+
self.assertEqual(
|
|
436
|
+
path,
|
|
437
|
+
"/run-cloud/sandboxes?state=running&tag=newly.role%3Aagent&tag=env%3Adev",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def test_set_tags_patches_and_keeps_null_delete_markers(self):
|
|
441
|
+
box = self.client.create()
|
|
442
|
+
box.set_tags({"newly.role": "agent", "gone": None})
|
|
443
|
+
method, path, body = self.recorder.calls[-1]
|
|
444
|
+
self.assertEqual(method, "PATCH")
|
|
445
|
+
self.assertEqual(path, "/run-cloud/sandboxes/sb_python/tags")
|
|
446
|
+
self.assertEqual(body, {"tags": {"newly.role": "agent", "gone": None}})
|
|
447
|
+
|
|
390
448
|
|
|
391
449
|
class CredentialTests(unittest.TestCase):
|
|
392
450
|
def setUp(self):
|
|
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
|