runtime-sdk 0.4.13__tar.gz → 0.4.15__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.13
3
+ Version: 0.4.15
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
@@ -35,10 +35,12 @@ uv tool upgrade runtime-sdk
35
35
 
36
36
  ## Configure
37
37
 
38
- The CLI talks to `http://127.0.0.1:8080` by default. Override it with:
38
+ The CLI talks to `https://api.runruntime.dev` by default.
39
+
40
+ For local or self-hosted Runtime, override it with:
39
41
 
40
42
  ```bash
41
- export RUNTIME_BASE_URL=https://api.runruntime.dev
43
+ export RUNTIME_BASE_URL=http://127.0.0.1:8080
42
44
  ```
43
45
 
44
46
  Or pass `--base-url` per command.
@@ -16,10 +16,12 @@ uv tool upgrade runtime-sdk
16
16
 
17
17
  ## Configure
18
18
 
19
- The CLI talks to `http://127.0.0.1:8080` by default. Override it with:
19
+ The CLI talks to `https://api.runruntime.dev` by default.
20
+
21
+ For local or self-hosted Runtime, override it with:
20
22
 
21
23
  ```bash
22
- export RUNTIME_BASE_URL=https://api.runruntime.dev
24
+ export RUNTIME_BASE_URL=http://127.0.0.1:8080
23
25
  ```
24
26
 
25
27
  Or pass `--base-url` per command.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "runtime-sdk"
3
- version = "0.4.13"
3
+ version = "0.4.15"
4
4
  description = "Runtime Python SDK and CLI"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -15,6 +15,7 @@ import termios
15
15
  import threading
16
16
  import time
17
17
  import tty
18
+ import webbrowser
18
19
  from datetime import datetime, timezone
19
20
  from pathlib import Path
20
21
  from typing import Any, Callable
@@ -132,7 +133,7 @@ class _UI:
132
133
 
133
134
  def build_parser() -> argparse.ArgumentParser:
134
135
  parser = argparse.ArgumentParser(prog="runtime")
135
- parser.add_argument("--base-url", help="Runtime API base URL")
136
+ parser.add_argument("--base-url", help="Override the Runtime API base URL")
136
137
  parser.add_argument("--version", action="version", version=f"runtime-sdk {__version__}")
137
138
 
138
139
  # Subcommand is optional: bare `runtime` defaults to `create`.
@@ -173,6 +174,14 @@ def build_parser() -> argparse.ArgumentParser:
173
174
  api_keys_revoke = api_keys_subparsers.add_parser("revoke", help="Revoke an API key")
174
175
  api_keys_revoke.add_argument("id", nargs="?", default=None, help="API key id")
175
176
 
177
+ integrations_cmd = subparsers.add_parser("integrations", help="Connect account-level integrations")
178
+ integrations_subparsers = integrations_cmd.add_subparsers(dest="integrations_action", required=True)
179
+ integrations_connect = integrations_subparsers.add_parser("connect", help="Connect an integration")
180
+ integrations_connect.add_argument("provider", choices=["github"])
181
+ integrations_connect.add_argument("--slug", default="github", help="Integration slug to create (default: github)")
182
+ integrations_connect.add_argument("--owner", default=None, help="GitHub username or org where the app is installed")
183
+ integrations_connect.add_argument("--no-open", action="store_true", help="Print the install URL instead of opening a browser")
184
+
176
185
  create_cmd = subparsers.add_parser("create", help="Create a new computer with the starter app published by default")
177
186
  create_cmd.add_argument(
178
187
  "name",
@@ -265,13 +274,12 @@ def main(argv: list[str] | None = None) -> int:
265
274
 
266
275
  try:
267
276
  config = load_config()
268
- base_url = (
269
- args.base_url
270
- or os.environ.get("RUNTIME_BASE_URL")
271
- or config.base_url
272
- or DEFAULT_BASE_URL
273
- )
274
- config.base_url = base_url
277
+ if args.base_url:
278
+ config.base_url = args.base_url
279
+ elif os.environ.get("RUNTIME_BASE_URL"):
280
+ config.base_url = str(os.environ.get("RUNTIME_BASE_URL"))
281
+ else:
282
+ config.base_url = config.configured_base_url or DEFAULT_BASE_URL
275
283
 
276
284
  # Bare `runtime` → first-run onboarding for interactive users, create otherwise.
277
285
  if args.command is None:
@@ -298,6 +306,10 @@ def main(argv: list[str] | None = None) -> int:
298
306
  if args.api_keys_action == "revoke":
299
307
  return handle_api_keys_revoke(config, args.id)
300
308
  return report_error("unknown api-keys command")
309
+ if args.command == "integrations":
310
+ if args.integrations_action == "connect":
311
+ return handle_integrations_connect(config, args.provider, args.slug, args.owner, no_open=args.no_open)
312
+ return report_error("unknown integrations command")
301
313
  if args.command == "create":
302
314
  return handle_create(
303
315
  config,
@@ -1408,6 +1420,68 @@ def handle_api_keys_revoke(config: RuntimeConfig, key_id: str | None) -> int:
1408
1420
 
1409
1421
 
1410
1422
 
1423
+ def handle_integrations_connect(
1424
+ config: RuntimeConfig,
1425
+ provider: str,
1426
+ slug: str | None,
1427
+ owner: str | None,
1428
+ *,
1429
+ no_open: bool = False,
1430
+ ) -> int:
1431
+ if (err := _require_api_key(config)) is not None:
1432
+ return err
1433
+ if provider != "github":
1434
+ return report_error(f"unsupported integration provider: {provider}")
1435
+
1436
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
1437
+ payload = _with_spinner("getting GitHub install link…", lambda: client.start_github_connect(slug=slug))
1438
+ install_url = str(payload.get("install_url") or "").strip()
1439
+ resolved_slug = str(payload.get("slug") or slug or "github").strip() or "github"
1440
+ if not install_url:
1441
+ return report_error("server did not return a GitHub install URL")
1442
+
1443
+ opened = False
1444
+ if not no_open:
1445
+ with contextlib.suppress(Exception):
1446
+ opened = bool(webbrowser.open(install_url))
1447
+
1448
+ if _interactive():
1449
+ action = "opened in your browser" if opened else "copy this URL into your browser"
1450
+ _UI.console().print(f"[cyan]→[/cyan] {action}: [link={install_url}]{install_url}[/link]")
1451
+ _UI.console().print("[dim]Install the RuntimeVM GitHub App, then come back here.[/dim]")
1452
+ input("Press Enter after the GitHub App is installed... ")
1453
+ if not owner:
1454
+ owner = _prompt_text("GitHub owner (username or org)")
1455
+ elif not owner:
1456
+ return report_error("--owner is required without a TTY")
1457
+
1458
+ resolved_owner = str(owner or "").strip()
1459
+ if not resolved_owner:
1460
+ return report_error("GitHub owner is required")
1461
+
1462
+ integration = _with_spinner(
1463
+ "connecting GitHub…",
1464
+ lambda: client.create_integration(
1465
+ slug=resolved_slug,
1466
+ provider="github",
1467
+ transport="scoped_credential",
1468
+ backend="native",
1469
+ credential_source="external_account",
1470
+ credential_ref=resolved_owner,
1471
+ config={},
1472
+ ),
1473
+ )
1474
+
1475
+ def render(result: dict[str, Any]) -> None:
1476
+ connected = result.get("integration") or {}
1477
+ _UI.console().print(
1478
+ f"[green]✓[/green] connected [bold]{connected.get('slug', resolved_slug)}[/bold] → [bold]{connected.get('credential_ref', resolved_owner)}[/bold]"
1479
+ )
1480
+
1481
+ return report_success({"integration": integration, "install_url": install_url}, render)
1482
+
1483
+
1484
+
1411
1485
  def handle_onboarding(config: RuntimeConfig) -> int:
1412
1486
  code = handle_login(config, None, None, None)
1413
1487
  if code != 0:
@@ -52,6 +52,36 @@ class RuntimeClient:
52
52
  def revoke_api_key(self, key_id: int) -> dict[str, Any]:
53
53
  return self._request("POST", f"/api/api-keys/{key_id}/revoke", auth_required=True)
54
54
 
55
+ # --- Integrations ---
56
+
57
+ def start_github_connect(self, *, slug: str | None = None) -> dict[str, Any]:
58
+ body: dict[str, Any] = {}
59
+ if slug:
60
+ body["slug"] = slug
61
+ return self._request("POST", "/api/integrations/github/connect", json=body, auth_required=True)
62
+
63
+ def create_integration(
64
+ self,
65
+ *,
66
+ slug: str,
67
+ provider: str,
68
+ transport: str,
69
+ backend: str,
70
+ credential_source: str,
71
+ credential_ref: str,
72
+ config: dict[str, Any] | None = None,
73
+ ) -> dict[str, Any]:
74
+ body: dict[str, Any] = {
75
+ "slug": slug,
76
+ "provider": provider,
77
+ "transport": transport,
78
+ "backend": backend,
79
+ "credential_source": credential_source,
80
+ "credential_ref": credential_ref,
81
+ "config": config or {},
82
+ }
83
+ return self._request("POST", "/api/integrations", json=body, auth_required=True)
84
+
55
85
  # --- Computers ---
56
86
 
57
87
  def create_computer(
@@ -6,7 +6,7 @@ from dataclasses import asdict, dataclass
6
6
  from pathlib import Path
7
7
 
8
8
 
9
- DEFAULT_BASE_URL = "http://127.0.0.1:8080"
9
+ DEFAULT_BASE_URL = "https://api.runruntime.dev"
10
10
 
11
11
 
12
12
  class RuntimeConfigError(RuntimeError):
@@ -23,6 +23,7 @@ class PendingSignup:
23
23
  @dataclass(slots=True)
24
24
  class RuntimeConfig:
25
25
  base_url: str = DEFAULT_BASE_URL
26
+ configured_base_url: str | None = None
26
27
  api_key: str | None = None
27
28
  pending_signup: PendingSignup | None = None
28
29
 
@@ -37,7 +38,7 @@ def config_path() -> Path:
37
38
  def load_config() -> RuntimeConfig:
38
39
  path = config_path()
39
40
  if not path.exists():
40
- return RuntimeConfig(base_url=os.environ.get("RUNTIME_BASE_URL", DEFAULT_BASE_URL))
41
+ return RuntimeConfig()
41
42
 
42
43
  try:
43
44
  data = json.loads(path.read_text())
@@ -48,9 +49,11 @@ def load_config() -> RuntimeConfig:
48
49
  if not isinstance(data, dict):
49
50
  raise RuntimeConfigError("config file must contain a JSON object")
50
51
  pending = data.get("pending_signup")
52
+ configured_base_url = data.get("base_url") or None
51
53
  try:
52
54
  return RuntimeConfig(
53
- base_url=data.get("base_url") or os.environ.get("RUNTIME_BASE_URL", DEFAULT_BASE_URL),
55
+ base_url=configured_base_url or DEFAULT_BASE_URL,
56
+ configured_base_url=configured_base_url,
54
57
  api_key=data.get("api_key"),
55
58
  pending_signup=PendingSignup(**pending) if pending else None,
56
59
  )
@@ -60,7 +63,13 @@ def load_config() -> RuntimeConfig:
60
63
 
61
64
  def save_config(config: RuntimeConfig) -> None:
62
65
  path = config_path()
63
- payload = asdict(config)
66
+ configured_base_url = config.configured_base_url
67
+ payload = {
68
+ "api_key": config.api_key,
69
+ "pending_signup": asdict(config.pending_signup) if config.pending_signup else None,
70
+ }
71
+ if configured_base_url is not None:
72
+ payload["base_url"] = configured_base_url
64
73
  try:
65
74
  path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
66
75
  path.write_text(json.dumps(payload, indent=2) + "\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runtime-sdk
3
- Version: 0.4.13
3
+ Version: 0.4.15
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
@@ -35,10 +35,12 @@ uv tool upgrade runtime-sdk
35
35
 
36
36
  ## Configure
37
37
 
38
- The CLI talks to `http://127.0.0.1:8080` by default. Override it with:
38
+ The CLI talks to `https://api.runruntime.dev` by default.
39
+
40
+ For local or self-hosted Runtime, override it with:
39
41
 
40
42
  ```bash
41
- export RUNTIME_BASE_URL=https://api.runruntime.dev
43
+ export RUNTIME_BASE_URL=http://127.0.0.1:8080
42
44
  ```
43
45
 
44
46
  Or pass `--base-url` per command.
File without changes