runtime-sdk 0.4.11__tar.gz → 0.4.12__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.11
3
+ Version: 0.4.12
4
4
  Summary: Runtime Python SDK and CLI
5
5
  Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
6
6
  Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
@@ -52,6 +52,9 @@ runtime verify 123456
52
52
  runtime whoami
53
53
  runtime logout
54
54
  runtime login --api-key rt_live_...
55
+ runtime api-keys list
56
+ runtime api-keys create "my laptop"
57
+ runtime api-keys revoke 123
55
58
 
56
59
  # Computers
57
60
  runtime create # creates a computer with the starter app already published
@@ -33,6 +33,9 @@ runtime verify 123456
33
33
  runtime whoami
34
34
  runtime logout
35
35
  runtime login --api-key rt_live_...
36
+ runtime api-keys list
37
+ runtime api-keys create "my laptop"
38
+ runtime api-keys revoke 123
36
39
 
37
40
  # Computers
38
41
  runtime create # creates a computer with the starter app already published
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runtime-sdk"
3
- version = "0.4.11"
3
+ version = "0.4.12"
4
4
  description = "Runtime Python SDK and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -165,6 +165,14 @@ def build_parser() -> argparse.ArgumentParser:
165
165
  subparsers.add_parser("whoami")
166
166
  subparsers.add_parser("logout")
167
167
 
168
+ api_keys_cmd = subparsers.add_parser("api-keys", help="Manage Runtime API keys")
169
+ api_keys_subparsers = api_keys_cmd.add_subparsers(dest="api_keys_action", required=True)
170
+ api_keys_subparsers.add_parser("list", aliases=["ls"], help="List API keys")
171
+ api_keys_create = api_keys_subparsers.add_parser("create", help="Create a new API key")
172
+ api_keys_create.add_argument("label", nargs="?", default=None, help="Human-friendly label for the new API key")
173
+ api_keys_revoke = api_keys_subparsers.add_parser("revoke", help="Revoke an API key")
174
+ api_keys_revoke.add_argument("id", nargs="?", default=None, help="API key id")
175
+
168
176
  create_cmd = subparsers.add_parser("create", help="Create a new computer with the starter app published by default")
169
177
  create_cmd.add_argument(
170
178
  "name",
@@ -280,6 +288,14 @@ def main(argv: list[str] | None = None) -> int:
280
288
  return handle_whoami(config)
281
289
  if args.command == "logout":
282
290
  return handle_logout(config)
291
+ if args.command == "api-keys":
292
+ if args.api_keys_action in ("list", "ls"):
293
+ return handle_api_keys_list(config)
294
+ if args.api_keys_action == "create":
295
+ return handle_api_keys_create(config, args.label)
296
+ if args.api_keys_action == "revoke":
297
+ return handle_api_keys_revoke(config, args.id)
298
+ return report_error("unknown api-keys command")
283
299
  if args.command == "create":
284
300
  return handle_create(
285
301
  config,
@@ -543,7 +559,7 @@ def _spawn_proxy_agent(base_url: str, api_key: str, computer_id: str, display_na
543
559
  finally:
544
560
  log_file.close()
545
561
 
546
- deadline = time.time() + 4.0
562
+ deadline = time.time() + max(4.0, DEFAULT_WARMUP_TIMEOUT + 2.0)
547
563
  state_path = _proxy_state_path(local_port)
548
564
  while time.time() < deadline:
549
565
  if state_path.exists():
@@ -715,8 +731,8 @@ def _computer_url_label(c: dict[str, Any]) -> str:
715
731
 
716
732
 
717
733
 
718
- def _computer_created_label(c: dict[str, Any]) -> str:
719
- raw = str(c.get("created_at") or "").strip()
734
+ def _relative_time_label(value: Any) -> str:
735
+ raw = str(value or "").strip()
720
736
  if not raw:
721
737
  return ""
722
738
  try:
@@ -738,6 +754,11 @@ def _computer_created_label(c: dict[str, Any]) -> str:
738
754
 
739
755
 
740
756
 
757
+ def _computer_created_label(c: dict[str, Any]) -> str:
758
+ return _relative_time_label(c.get("created_at"))
759
+
760
+
761
+
741
762
  def _computer_status_style(status: str) -> str:
742
763
  if "cold" in status:
743
764
  return "bold cyan"
@@ -1072,6 +1093,84 @@ def _render_whoami_panel(payload: dict[str, Any]) -> None:
1072
1093
  _UI.console().print(Panel(body, title="whoami", border_style="cyan", expand=False))
1073
1094
 
1074
1095
 
1096
+
1097
+ def _api_key_status_label(key: dict[str, Any]) -> str:
1098
+ if key.get("revoked_at"):
1099
+ return "revoked"
1100
+ if key.get("last_used_at"):
1101
+ return "used"
1102
+ return "unused"
1103
+
1104
+
1105
+
1106
+ def _api_key_status_style(status: str) -> str:
1107
+ if status == "revoked":
1108
+ return "bold red"
1109
+ if status == "used":
1110
+ return "bold green"
1111
+ return "yellow"
1112
+
1113
+
1114
+
1115
+ def _render_api_keys_table(payload: dict[str, Any]) -> None:
1116
+ mods = _UI.rich()
1117
+ Table = mods["Table"]
1118
+ api_keys = payload.get("api_keys") or []
1119
+ if not isinstance(api_keys, list) or not api_keys:
1120
+ _UI.console().print("[yellow]no api keys found[/yellow]")
1121
+ return
1122
+
1123
+ table = Table(title="API keys", expand=True, box=mods["box"].SIMPLE_HEAVY)
1124
+ table.add_column("ID", style="dim", no_wrap=True)
1125
+ table.add_column("Label", style="bold white")
1126
+ table.add_column("Prefix", style="white", no_wrap=True)
1127
+ table.add_column("Created", style="dim", no_wrap=True, justify="right")
1128
+ table.add_column("Last used", style="dim", no_wrap=True, justify="right")
1129
+ table.add_column("Status", no_wrap=True)
1130
+
1131
+ for entry in api_keys:
1132
+ if not isinstance(entry, dict):
1133
+ continue
1134
+ status = _api_key_status_label(entry)
1135
+ style = _api_key_status_style(status)
1136
+ table.add_row(
1137
+ str(entry.get("id") or "—"),
1138
+ str(entry.get("label") or "—"),
1139
+ str(entry.get("key_prefix") or "—"),
1140
+ _relative_time_label(entry.get("created_at")) or "—",
1141
+ _relative_time_label(entry.get("last_used_at")) or "—",
1142
+ f"[{style}]{status}[/{style}]",
1143
+ )
1144
+
1145
+ _UI.console().print(table)
1146
+
1147
+
1148
+
1149
+ def _render_api_key_created(payload: dict[str, Any]) -> None:
1150
+ mods = _UI.rich()
1151
+ Panel, Text = mods["Panel"], mods["Text"]
1152
+ key = payload.get("key") or {}
1153
+ secret = str(payload.get("api_key") or "")
1154
+ body = Text()
1155
+ rows = [
1156
+ ("id", key.get("id")),
1157
+ ("label", key.get("label")),
1158
+ ("prefix", key.get("key_prefix")),
1159
+ ]
1160
+ for label, value in rows:
1161
+ if value is None:
1162
+ continue
1163
+ body.append(f"{label:<12}", style="dim")
1164
+ body.append(f"{value}\n")
1165
+ if secret:
1166
+ body.append("save now ", style="bold yellow")
1167
+ body.append("this secret is only shown once\n", style="yellow")
1168
+ body.append("api key ", style="dim")
1169
+ body.append(secret, style="bold white")
1170
+ body.append("\n")
1171
+ _UI.console().print(Panel(body, title="api key created", border_style="cyan", expand=False))
1172
+
1173
+
1075
1174
  # --------------------------------------------------------------------------- #
1076
1175
  # Command handlers #
1077
1176
  # --------------------------------------------------------------------------- #
@@ -1220,6 +1319,56 @@ def handle_logout(config: RuntimeConfig) -> int:
1220
1319
  return report_success({"message": message}, render)
1221
1320
 
1222
1321
 
1322
+
1323
+ def handle_api_keys_list(config: RuntimeConfig) -> int:
1324
+ if (err := _require_api_key(config)) is not None:
1325
+ return err
1326
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1327
+ return report_success(client.list_api_keys(), _render_api_keys_table)
1328
+
1329
+
1330
+
1331
+ def handle_api_keys_create(config: RuntimeConfig, label: str | None) -> int:
1332
+ if (err := _require_api_key(config)) is not None:
1333
+ return err
1334
+ if label is None:
1335
+ if not _interactive():
1336
+ return report_error("label is required")
1337
+ label = _prompt_text("API key label")
1338
+ if not label:
1339
+ return report_error("label is required")
1340
+
1341
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1342
+ result = client.create_api_key(label)
1343
+ return report_success(result, _render_api_key_created)
1344
+
1345
+
1346
+
1347
+ def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
1348
+ if (err := _require_api_key(config)) is not None:
1349
+ return err
1350
+ if key_id is None:
1351
+ if not _interactive():
1352
+ return report_error("api key id is required")
1353
+ key_id = _prompt_text("API key id")
1354
+ if not key_id:
1355
+ return report_error("api key id is required")
1356
+
1357
+ try:
1358
+ resolved_key_id = int(str(key_id).strip())
1359
+ except ValueError:
1360
+ return report_error("api key id must be a number")
1361
+
1362
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1363
+ client.revoke_api_key(resolved_key_id)
1364
+
1365
+ def render(_: dict[str, Any]) -> None:
1366
+ _UI.console().print(f"[green]✓[/green] api key {resolved_key_id} revoked")
1367
+
1368
+ return report_success({"id": resolved_key_id, "message": f"api key {resolved_key_id} revoked"}, render)
1369
+
1370
+
1371
+
1223
1372
  def handle_create(
1224
1373
  config: RuntimeConfig,
1225
1374
  name: str | None,
@@ -1633,6 +1782,10 @@ def handle_proxy_start(config: RuntimeConfig, computer_id: str | None, specs: li
1633
1782
  if started_ports:
1634
1783
  _stop_proxy_ports(started_ports, stop_all=False)
1635
1784
  return report_error(str(exc), status_code=exc.status_code)
1785
+ except Exception as exc:
1786
+ if started_ports:
1787
+ _stop_proxy_ports(started_ports, stop_all=False)
1788
+ return report_error(str(exc))
1636
1789
  started_ports.append(local_port)
1637
1790
  started.append({"computer_id": resolved_id, "computer": computer_id, "local_port": local_port, "remote_port": remote_port})
1638
1791
 
@@ -43,6 +43,15 @@ class RuntimeClient:
43
43
  def logout(self) -> dict[str, Any]:
44
44
  return self._request("POST", "/api/auth/logout", auth_required=True)
45
45
 
46
+ def list_api_keys(self) -> dict[str, Any]:
47
+ return self._request("GET", "/api/api-keys", auth_required=True)
48
+
49
+ def create_api_key(self, label: str) -> dict[str, Any]:
50
+ return self._request("POST", "/api/api-keys", json={"label": label}, auth_required=True)
51
+
52
+ def revoke_api_key(self, key_id: int) -> dict[str, Any]:
53
+ return self._request("POST", f"/api/api-keys/{key_id}/revoke", auth_required=True)
54
+
46
55
  # --- Computers ---
47
56
 
48
57
  def create_computer(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.11
3
+ Version: 0.4.12
4
4
  Summary: Runtime Python SDK and CLI
5
5
  Project-URL: Repository, https://github.com/The-Money-Company-Limited/runtimevm
6
6
  Project-URL: Issues, https://github.com/The-Money-Company-Limited/runtimevm/issues
@@ -52,6 +52,9 @@ runtime verify 123456
52
52
  runtime whoami
53
53
  runtime logout
54
54
  runtime login --api-key rt_live_...
55
+ runtime api-keys list
56
+ runtime api-keys create "my laptop"
57
+ runtime api-keys revoke 123
55
58
 
56
59
  # Computers
57
60
  runtime create # creates a computer with the starter app already published
File without changes