runtime-sdk 0.4.15__tar.gz → 0.4.16__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.15
3
+ Version: 0.4.16
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runtime-sdk"
3
- version = "0.4.15"
3
+ version = "0.4.16"
4
4
  description = "Runtime Python SDK and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -174,6 +174,20 @@ def build_parser() -> argparse.ArgumentParser:
174
174
  api_keys_revoke = api_keys_subparsers.add_parser("revoke", help="Revoke an API key")
175
175
  api_keys_revoke.add_argument("id", nargs="?", default=None, help="API key id")
176
176
 
177
+ secrets_cmd = subparsers.add_parser("secrets", help="Manage account-level secret sets")
178
+ secrets_subparsers = secrets_cmd.add_subparsers(dest="secrets_action", required=True)
179
+ secrets_subparsers.add_parser("list", aliases=["ls"], help="List secret sets")
180
+ secrets_show = secrets_subparsers.add_parser("show", help="Show one secret set")
181
+ secrets_show.add_argument("slug", nargs="?", default=None, help="Secret set slug")
182
+ secrets_set = secrets_subparsers.add_parser("set", help="Create or replace a secret set from KEY=VALUE pairs")
183
+ secrets_set.add_argument("slug", nargs="?", default=None, help="Secret set slug")
184
+ secrets_set.add_argument("pairs", nargs="*", help="KEY=VALUE pairs")
185
+ secrets_import = secrets_subparsers.add_parser("import", help="Create or replace a secret set from a .env file")
186
+ secrets_import.add_argument("slug", nargs="?", default=None, help="Secret set slug")
187
+ secrets_import.add_argument("--env-file", required=True, help="Path to .env file")
188
+ secrets_delete = secrets_subparsers.add_parser("delete", aliases=["rm"], help="Delete a secret set")
189
+ secrets_delete.add_argument("slug", nargs="?", default=None, help="Secret set slug")
190
+
177
191
  integrations_cmd = subparsers.add_parser("integrations", help="Connect account-level integrations")
178
192
  integrations_subparsers = integrations_cmd.add_subparsers(dest="integrations_action", required=True)
179
193
  integrations_connect = integrations_subparsers.add_parser("connect", help="Connect an integration")
@@ -306,6 +320,18 @@ def main(argv: list[str] | None = None) -> int:
306
320
  if args.api_keys_action == "revoke":
307
321
  return handle_api_keys_revoke(config, args.id)
308
322
  return report_error("unknown api-keys command")
323
+ if args.command == "secrets":
324
+ if args.secrets_action in ("list", "ls"):
325
+ return handle_secrets_list(config)
326
+ if args.secrets_action == "show":
327
+ return handle_secrets_show(config, args.slug)
328
+ if args.secrets_action == "set":
329
+ return handle_secrets_set(config, args.slug, args.pairs)
330
+ if args.secrets_action == "import":
331
+ return handle_secrets_import(config, args.slug, args.env_file)
332
+ if args.secrets_action in ("delete", "rm"):
333
+ return handle_secrets_delete(config, args.slug)
334
+ return report_error("unknown secrets command")
309
335
  if args.command == "integrations":
310
336
  if args.integrations_action == "connect":
311
337
  return handle_integrations_connect(config, args.provider, args.slug, args.owner, no_open=args.no_open)
@@ -1185,6 +1211,45 @@ def _render_api_key_created(payload: dict[str, Any]) -> None:
1185
1211
  _UI.console().print(Panel(body, title="api key created", border_style="cyan", expand=False))
1186
1212
 
1187
1213
 
1214
+
1215
+ def _render_secret_sets_table(payload: dict[str, Any]) -> None:
1216
+ mods = _UI.rich()
1217
+ Table = mods["Table"]
1218
+ secret_sets = payload.get("secret_sets") or []
1219
+ if not isinstance(secret_sets, list) or not secret_sets:
1220
+ _UI.console().print("[yellow]no secret sets found[/yellow]")
1221
+ return
1222
+
1223
+ table = Table(title="Secret sets", expand=True, box=mods["box"].SIMPLE_HEAVY)
1224
+ table.add_column("Slug", style="bold white")
1225
+ table.add_column("Keys", style="white")
1226
+ table.add_column("Updated", style="dim", no_wrap=True, justify="right")
1227
+
1228
+ for entry in secret_sets:
1229
+ if not isinstance(entry, dict):
1230
+ continue
1231
+ keys = entry.get("keys") or []
1232
+ table.add_row(
1233
+ str(entry.get("slug") or "—"),
1234
+ ", ".join(str(key) for key in keys) or "—",
1235
+ _relative_time_label(entry.get("updated_at")) or "—",
1236
+ )
1237
+
1238
+ _UI.console().print(table)
1239
+
1240
+
1241
+
1242
+ def _render_secret_set_panel(payload: dict[str, Any]) -> None:
1243
+ mods = _UI.rich()
1244
+ Panel, Text = mods["Panel"], mods["Text"]
1245
+ body = Text()
1246
+ body.append(f"{'slug':<12}", style="dim")
1247
+ body.append(f"{payload.get('slug') or '—'}\n")
1248
+ body.append(f"{'keys':<12}", style="dim")
1249
+ body.append(", ".join(str(key) for key in (payload.get("keys") or [])) or "—")
1250
+ _UI.console().print(Panel(body, title="secret set", border_style="cyan", expand=False))
1251
+
1252
+
1188
1253
  # --------------------------------------------------------------------------- #
1189
1254
  # Command handlers #
1190
1255
  # --------------------------------------------------------------------------- #
@@ -1420,6 +1485,138 @@ def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
1420
1485
 
1421
1486
 
1422
1487
 
1488
+ def _coerce_secret_set_slug(slug: str | None) -> str | None:
1489
+ value = str(slug or "").strip()
1490
+ return value or None
1491
+
1492
+
1493
+
1494
+ def _parse_secret_pairs(pairs: list[str] | None) -> dict[str, str]:
1495
+ values: dict[str, str] = {}
1496
+ for pair in pairs or []:
1497
+ if "=" not in pair:
1498
+ raise RuntimeConfigError(f"invalid secret pair: {pair}")
1499
+ key, value = pair.split("=", 1)
1500
+ key = key.strip()
1501
+ if not key:
1502
+ raise RuntimeConfigError(f"invalid secret pair: {pair}")
1503
+ values[key] = value
1504
+ return values
1505
+
1506
+
1507
+
1508
+ def _parse_env_file(path: str) -> dict[str, str]:
1509
+ try:
1510
+ lines = Path(path).read_text(encoding="utf-8").splitlines()
1511
+ except OSError as exc:
1512
+ raise RuntimeConfigError(f"failed to read env file: {exc}") from exc
1513
+ values: dict[str, str] = {}
1514
+ for raw_line in lines:
1515
+ line = raw_line.strip()
1516
+ if not line or line.startswith("#"):
1517
+ continue
1518
+ if line.startswith("export "):
1519
+ line = line[len("export "):].strip()
1520
+ if "=" not in line:
1521
+ raise RuntimeConfigError(f"invalid env line: {raw_line}")
1522
+ key, value = line.split("=", 1)
1523
+ key = key.strip()
1524
+ if not key:
1525
+ raise RuntimeConfigError(f"invalid env line: {raw_line}")
1526
+ value = value.strip()
1527
+ if value and value[0] == value[-1] and value[0] in {'\"', "'"}:
1528
+ value = value[1:-1]
1529
+ values[key] = value
1530
+ return values
1531
+
1532
+
1533
+
1534
+ def handle_secrets_list(config: RuntimeConfig) -> int:
1535
+ if (err := _require_api_key(config)) is not None:
1536
+ return err
1537
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1538
+ return report_success(client.list_secret_sets(), _render_secret_sets_table)
1539
+
1540
+
1541
+
1542
+ def handle_secrets_show(config: RuntimeConfig, slug: str | None) -> int:
1543
+ if (err := _require_api_key(config)) is not None:
1544
+ return err
1545
+ resolved_slug = _coerce_secret_set_slug(slug)
1546
+ if resolved_slug is None:
1547
+ if not _interactive():
1548
+ return report_error("secret set slug is required")
1549
+ resolved_slug = _prompt_text("Secret set slug")
1550
+ if not resolved_slug:
1551
+ return report_error("secret set slug is required")
1552
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1553
+ return report_success(client.get_secret_set(resolved_slug), _render_secret_set_panel)
1554
+
1555
+
1556
+
1557
+ def handle_secrets_set(config: RuntimeConfig, slug: str | None, pairs: list[str] | None) -> int:
1558
+ if (err := _require_api_key(config)) is not None:
1559
+ return err
1560
+ resolved_slug = _coerce_secret_set_slug(slug)
1561
+ if resolved_slug is None:
1562
+ if not _interactive():
1563
+ return report_error("secret set slug is required")
1564
+ resolved_slug = _prompt_text("Secret set slug")
1565
+ if not resolved_slug:
1566
+ return report_error("secret set slug is required")
1567
+ values = _parse_secret_pairs(pairs)
1568
+ if not values:
1569
+ return report_error("at least one KEY=VALUE pair is required")
1570
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1571
+ secret_set = client.put_secret_set(resolved_slug, values)
1572
+ return report_success(
1573
+ {"secret_set": secret_set},
1574
+ lambda payload: _UI.console().print(f"[green]✓[/green] saved [bold]{payload['secret_set'].get('slug', resolved_slug)}[/bold]"),
1575
+ )
1576
+
1577
+
1578
+
1579
+ def handle_secrets_import(config: RuntimeConfig, slug: str | None, env_file: str) -> int:
1580
+ if (err := _require_api_key(config)) is not None:
1581
+ return err
1582
+ resolved_slug = _coerce_secret_set_slug(slug)
1583
+ if resolved_slug is None:
1584
+ if not _interactive():
1585
+ return report_error("secret set slug is required")
1586
+ resolved_slug = _prompt_text("Secret set slug")
1587
+ if not resolved_slug:
1588
+ return report_error("secret set slug is required")
1589
+ values = _parse_env_file(env_file)
1590
+ if not values:
1591
+ return report_error("env file did not contain any secrets")
1592
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1593
+ secret_set = client.put_secret_set(resolved_slug, values)
1594
+ return report_success(
1595
+ {"secret_set": secret_set},
1596
+ lambda payload: _UI.console().print(f"[green]✓[/green] saved [bold]{payload['secret_set'].get('slug', resolved_slug)}[/bold] from [bold]{env_file}[/bold]"),
1597
+ )
1598
+
1599
+
1600
+
1601
+ def handle_secrets_delete(config: RuntimeConfig, slug: str | None) -> int:
1602
+ if (err := _require_api_key(config)) is not None:
1603
+ return err
1604
+ resolved_slug = _coerce_secret_set_slug(slug)
1605
+ if resolved_slug is None:
1606
+ if not _interactive():
1607
+ return report_error("secret set slug is required")
1608
+ resolved_slug = _prompt_text("Secret set slug")
1609
+ if not resolved_slug:
1610
+ return report_error("secret set slug is required")
1611
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1612
+ client.delete_secret_set(resolved_slug)
1613
+ return report_success(
1614
+ {"slug": resolved_slug, "deleted": True},
1615
+ lambda payload: _UI.console().print(f"[green]✓[/green] deleted [bold]{payload['slug']}[/bold]"),
1616
+ )
1617
+
1618
+
1619
+
1423
1620
  def handle_integrations_connect(
1424
1621
  config: RuntimeConfig,
1425
1622
  provider: str,
@@ -82,6 +82,18 @@ class RuntimeClient:
82
82
  }
83
83
  return self._request("POST", "/api/integrations", json=body, auth_required=True)
84
84
 
85
+ def list_secret_sets(self) -> dict[str, Any]:
86
+ return self._request("GET", "/api/secrets", auth_required=True)
87
+
88
+ def get_secret_set(self, slug: str) -> dict[str, Any]:
89
+ return self._request("GET", f"/api/secrets/{slug}", auth_required=True)
90
+
91
+ def put_secret_set(self, slug: str, values: dict[str, str]) -> dict[str, Any]:
92
+ return self._request("PUT", f"/api/secrets/{slug}", json={"values": values}, auth_required=True)
93
+
94
+ def delete_secret_set(self, slug: str) -> dict[str, Any]:
95
+ return self._request("DELETE", f"/api/secrets/{slug}", auth_required=True)
96
+
85
97
  # --- Computers ---
86
98
 
87
99
  def create_computer(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.15
3
+ Version: 0.4.16
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
File without changes
File without changes