runcloud-sdk 0.8.2__tar.gz → 0.9.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.2/runcloud_sdk.egg-info → runcloud_sdk-0.9.0}/PKG-INFO +16 -1
  2. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/README.md +15 -0
  3. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/pyproject.toml +1 -1
  4. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/client.py +30 -4
  5. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0/runcloud_sdk.egg-info}/PKG-INFO +16 -1
  6. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/tests/test_sdk.py +44 -3
  7. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/LICENSE +0 -0
  8. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/__init__.py +0 -0
  9. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/compat/__init__.py +0 -0
  10. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/compat/blaxel.py +0 -0
  11. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/compat/daytona.py +0 -0
  12. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/compat/e2b.py +0 -0
  13. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/compat/modal.py +0 -0
  14. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/compat/sprites.py +0 -0
  15. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud/compat/vercel.py +0 -0
  16. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud_sdk.egg-info/SOURCES.txt +0 -0
  17. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud_sdk.egg-info/dependency_links.txt +0 -0
  18. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud_sdk.egg-info/requires.txt +0 -0
  19. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/runcloud_sdk.egg-info/top_level.txt +0 -0
  20. {runcloud_sdk-0.8.2 → runcloud_sdk-0.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runcloud-sdk
3
- Version: 0.8.2
3
+ Version: 0.9.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.2"
3
+ version = "0.9.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"
@@ -20,7 +20,7 @@ from websockets.sync.client import connect as websocket_connect
20
20
  Transport = Callable[[str, str, Any], Any]
21
21
  StreamTransport = Callable[[str, dict[str, Any]], Any]
22
22
  DEFAULT_API_URL = "https://api.run.cloud"
23
- USER_AGENT = "runcloud-python/0.8.2"
23
+ USER_AGENT = "runcloud-python/0.8.3"
24
24
  FILE_CHUNK_BYTES = 512 * 1024
25
25
 
26
26
 
@@ -253,7 +253,9 @@ class Client:
253
253
  explicit_api_key = _nonempty_string(api_key)
254
254
  environment_api_key = _nonempty_string(
255
255
  os.environ.get("RUN_CLOUD_API_KEY")
256
- ) or _nonempty_string(os.environ.get("RUN_CLOUD_API_TOKEN"))
256
+ ) or _nonempty_string(
257
+ os.environ.get("RUN_CLOUD_API_TOKEN")
258
+ ) or _nonempty_string(os.environ.get("RUN_CLOUD_CI_ENVIRONMENT_TOKEN"))
257
259
  saved_credentials = (
258
260
  None
259
261
  if explicit_api_key or environment_api_key
@@ -370,8 +372,17 @@ class Client:
370
372
  sid = urllib.parse.quote(sandbox_id, safe="")
371
373
  return Sandbox(self, self.request("GET", f"/run-cloud/sandboxes/{sid}"))
372
374
 
373
- def list(self, state: Optional[str] = None) -> list["Sandbox"]:
374
- query = f"?state={urllib.parse.quote(state, safe='')}" if state else ""
375
+ def list(
376
+ self, state: Optional[str] = None, name: Optional[str] = None
377
+ ) -> list["Sandbox"]:
378
+ # Both filters are applied server-side, so finding a sandbox by the name
379
+ # you gave it does not mean paging the whole fleet and matching locally.
380
+ filters = [
381
+ (key, value)
382
+ for key, value in (("state", state), ("name", name))
383
+ if value
384
+ ]
385
+ query = "?" + urllib.parse.urlencode(filters) if filters else ""
375
386
  payload = self.request("GET", "/run-cloud/sandboxes" + query) or {}
376
387
  return [Sandbox(self, item) for item in payload.get("items", [])]
377
388
 
@@ -546,6 +557,21 @@ class Sandbox:
546
557
  )
547
558
  return self
548
559
 
560
+ def rename(self, name: str) -> "Sandbox":
561
+ """Relabel the sandbox.
562
+
563
+ The name is a label you own and the filter ``list(name=...)`` matches on,
564
+ so this is how a sandbox gets re-tagged without destroying it. Works in
565
+ any state but destroyed; an empty name clears the label. Names are not
566
+ unique and are capped at 128 characters.
567
+ """
568
+ self._data = self._client.request(
569
+ "POST",
570
+ f"/run-cloud/sandboxes/{urllib.parse.quote(self.id, safe='')}/name",
571
+ {"name": name},
572
+ )
573
+ return self
574
+
549
575
  def set_timeout(self, seconds: int) -> "Sandbox":
550
576
  self._data = self._client.request(
551
577
  "POST",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runcloud-sdk
3
- Version: 0.8.2
3
+ Version: 0.9.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),
@@ -338,7 +367,7 @@ class SDKTests(unittest.TestCase):
338
367
  Client(api_key="rc_live_test").request("GET", "/run-cloud/account")
339
368
 
340
369
  request = urlopen.call_args.args[0]
341
- self.assertEqual(request.get_header("User-agent"), "runcloud-python/0.8.2")
370
+ self.assertEqual(request.get_header("User-agent"), "runcloud-python/0.8.3")
342
371
 
343
372
  def test_exec_stream_opens_authenticated_websocket_and_sends_command(self):
344
373
  session = WebSocketSession()
@@ -368,7 +397,7 @@ class SDKTests(unittest.TestCase):
368
397
  connect.assert_called_once_with(
369
398
  "wss://api.example.test/run-cloud/sandboxes/sb_1/exec/stream?mode=command",
370
399
  additional_headers={"Authorization": "Bearer rc_live_test"},
371
- user_agent_header="runcloud-python/0.8.2",
400
+ user_agent_header="runcloud-python/0.8.3",
372
401
  open_timeout=60.0,
373
402
  close_timeout=5,
374
403
  max_size=8 << 20,
@@ -398,7 +427,12 @@ class CredentialTests(unittest.TestCase):
398
427
  clear=False,
399
428
  )
400
429
  self.environment.start()
401
- for name in ("RUN_CLOUD_API_KEY", "RUN_CLOUD_API_TOKEN", "RUN_CLOUD_API_URL"):
430
+ for name in (
431
+ "RUN_CLOUD_API_KEY",
432
+ "RUN_CLOUD_API_TOKEN",
433
+ "RUN_CLOUD_CI_ENVIRONMENT_TOKEN",
434
+ "RUN_CLOUD_API_URL",
435
+ ):
402
436
  os.environ.pop(name, None)
403
437
 
404
438
  def tearDown(self):
@@ -458,6 +492,13 @@ class CredentialTests(unittest.TestCase):
458
492
  self.assertEqual(client.api_key, "environment-key")
459
493
  self.assertEqual(client.api_url, "https://api.run.cloud")
460
494
 
495
+ def test_ci_environment_capability_is_used_without_an_api_key(self):
496
+ os.environ["RUN_CLOUD_CI_ENVIRONMENT_TOKEN"] = "rce_environment"
497
+
498
+ client = Client()
499
+
500
+ self.assertEqual(client.api_key, "rce_environment")
501
+
461
502
  def test_api_url_override_applies_to_saved_login(self):
462
503
  self.save_credentials()
463
504
  os.environ["RUN_CLOUD_API_URL"] = "https://override.example/"
File without changes
File without changes