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.
Files changed (20) hide show
  1. {runcloud_sdk-0.8.3/runcloud_sdk.egg-info → runcloud_sdk-0.10.0}/PKG-INFO +16 -1
  2. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/README.md +15 -0
  3. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/pyproject.toml +1 -1
  4. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/client.py +52 -2
  5. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0/runcloud_sdk.egg-info}/PKG-INFO +16 -1
  6. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/tests/test_sdk.py +58 -0
  7. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/LICENSE +0 -0
  8. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/__init__.py +0 -0
  9. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/__init__.py +0 -0
  10. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/blaxel.py +0 -0
  11. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/daytona.py +0 -0
  12. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/e2b.py +0 -0
  13. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/modal.py +0 -0
  14. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/sprites.py +0 -0
  15. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud/compat/vercel.py +0 -0
  16. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/SOURCES.txt +0 -0
  17. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/dependency_links.txt +0 -0
  18. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/requires.txt +0 -0
  19. {runcloud_sdk-0.8.3 → runcloud_sdk-0.10.0}/runcloud_sdk.egg-info/top_level.txt +0 -0
  20. {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.8.3
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:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runcloud-sdk"
3
- version = "0.8.3"
3
+ version = "0.10.0"
4
4
  description = "Python SDK for run.cloud, including sandbox-provider compatibility adapters"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -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(self, state: Optional[str] = None) -> list["Sandbox"]:
376
- query = f"?state={urllib.parse.quote(state, safe='')}" if state else ""
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.8.3
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