runtime-sdk 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ from runtime_sdk.client import RuntimeAPIError, RuntimeClient
2
+
3
+ __all__ = ["RuntimeAPIError", "RuntimeClient"]
runtime_sdk/cli.py ADDED
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from typing import Any
8
+
9
+ from runtime_sdk.client import RuntimeAPIError, RuntimeClient
10
+ from runtime_sdk.config import DEFAULT_BASE_URL, PendingSignup, RuntimeConfig, load_config, save_config
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(prog="runtime")
15
+ parser.add_argument("--base-url", help="Runtime API base URL")
16
+
17
+ subparsers = parser.add_subparsers(dest="command", required=True)
18
+
19
+ signup = subparsers.add_parser("signup")
20
+ signup.add_argument("email")
21
+ signup.add_argument("--name", required=True)
22
+
23
+ verify = subparsers.add_parser("verify")
24
+ verify.add_argument("code")
25
+ verify.add_argument("--flow-id", type=int)
26
+
27
+ login = subparsers.add_parser("login")
28
+ login.add_argument("api_key")
29
+
30
+ subparsers.add_parser("whoami")
31
+ subparsers.add_parser("logout")
32
+
33
+ return parser
34
+
35
+
36
+ def main(argv: list[str] | None = None) -> int:
37
+ parser = build_parser()
38
+ args = parser.parse_args(argv)
39
+
40
+ try:
41
+ config = load_config()
42
+ base_url = args.base_url or os.environ.get("RUNTIME_BASE_URL") or config.base_url or DEFAULT_BASE_URL
43
+ config.base_url = base_url
44
+
45
+ if args.command == "signup":
46
+ return handle_signup(config, args.email, args.name)
47
+ if args.command == "verify":
48
+ return handle_verify(config, args.code, args.flow_id)
49
+ if args.command == "login":
50
+ return handle_login(config, args.api_key)
51
+ if args.command == "whoami":
52
+ return handle_whoami(config)
53
+ if args.command == "logout":
54
+ return handle_logout(config)
55
+ except RuntimeAPIError as exc:
56
+ return write_json({"success": False, "error": str(exc), "status_code": exc.status_code}, error=True)
57
+
58
+ return write_json({"success": False, "error": "unknown command"}, error=True)
59
+
60
+
61
+ def handle_signup(config: RuntimeConfig, email: str, name: str) -> int:
62
+ client = RuntimeClient(base_url=config.base_url)
63
+ result = client.signup(email, name)
64
+ config.pending_signup = PendingSignup(
65
+ flow_id=int(result["flow_id"]),
66
+ email=email,
67
+ expires_at=result.get("expires_at"),
68
+ )
69
+ save_config(config)
70
+ return write_json({"success": True, **result})
71
+
72
+
73
+ def handle_verify(config: RuntimeConfig, code: str, flow_id: int | None) -> int:
74
+ pending = config.pending_signup
75
+ resolved_flow_id = flow_id or (pending.flow_id if pending else None)
76
+ if not resolved_flow_id:
77
+ return write_json(
78
+ {"success": False, "error": "missing flow id; run signup first or pass --flow-id"},
79
+ error=True,
80
+ )
81
+
82
+ client = RuntimeClient(base_url=config.base_url)
83
+ result = client.verify(resolved_flow_id, code)
84
+ api_key = result.get("api_key")
85
+ if api_key:
86
+ config.api_key = api_key
87
+ config.pending_signup = None
88
+ save_config(config)
89
+ return write_json({"success": True, **result})
90
+
91
+
92
+ def handle_login(config: RuntimeConfig, api_key: str) -> int:
93
+ config.api_key = api_key
94
+ save_config(config)
95
+ return write_json({"success": True, "message": "api key saved"})
96
+
97
+
98
+ def handle_whoami(config: RuntimeConfig) -> int:
99
+ if not config.api_key:
100
+ return write_json({"success": False, "error": "missing api key; run verify or login first"}, error=True)
101
+
102
+ client = RuntimeClient(base_url=config.base_url, api_key=config.api_key)
103
+ result = client.whoami()
104
+ return write_json({"success": True, **result})
105
+
106
+
107
+ def handle_logout(config: RuntimeConfig) -> int:
108
+ config.api_key = None
109
+ config.pending_signup = None
110
+ save_config(config)
111
+ return write_json({"success": True, "message": "logged out"})
112
+
113
+
114
+ def write_json(payload: dict[str, Any], *, error: bool = False) -> int:
115
+ stream = sys.stderr if error else sys.stdout
116
+ stream.write(json.dumps(payload) + "\n")
117
+ return 1 if error else 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ raise SystemExit(main())
runtime_sdk/client.py ADDED
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+
9
+ class RuntimeAPIError(RuntimeError):
10
+ def __init__(self, message: str, status_code: int | None = None) -> None:
11
+ super().__init__(message)
12
+ self.status_code = status_code
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class RuntimeClient:
17
+ base_url: str
18
+ api_key: str | None = None
19
+ timeout: float = 10.0
20
+ transport: httpx.BaseTransport | None = None
21
+
22
+ def __post_init__(self) -> None:
23
+ self.base_url = self.base_url.rstrip("/")
24
+
25
+ def signup(self, email: str, name: str) -> dict[str, Any]:
26
+ return self._request("POST", "/api/auth/start", json={"email": email, "name": name})
27
+
28
+ def verify(self, flow_id: int, code: str) -> dict[str, Any]:
29
+ return self._request("POST", "/api/auth/verify", json={"flow_id": flow_id, "code": code})
30
+
31
+ def whoami(self) -> dict[str, Any]:
32
+ return self._request("GET", "/api/auth/whoami", auth_required=True)
33
+
34
+ def _request(
35
+ self,
36
+ method: str,
37
+ path: str,
38
+ *,
39
+ json: dict[str, Any] | None = None,
40
+ auth_required: bool = False,
41
+ ) -> dict[str, Any]:
42
+ headers: dict[str, str] = {}
43
+ if auth_required:
44
+ if not self.api_key:
45
+ raise RuntimeAPIError("missing api key")
46
+ headers["Authorization"] = f"Bearer {self.api_key}"
47
+
48
+ try:
49
+ with httpx.Client(
50
+ base_url=self.base_url,
51
+ timeout=self.timeout,
52
+ transport=self.transport,
53
+ ) as client:
54
+ response = client.request(method, path, json=json, headers=headers)
55
+ except httpx.HTTPError as exc:
56
+ raise RuntimeAPIError(f"request failed: {exc}") from exc
57
+
58
+ payload = self._decode_response(response)
59
+ if response.is_success:
60
+ return payload
61
+
62
+ message = payload.get("error") or payload.get("message") or f"http {response.status_code}"
63
+ raise RuntimeAPIError(str(message), status_code=response.status_code)
64
+
65
+ @staticmethod
66
+ def _decode_response(response: httpx.Response) -> dict[str, Any]:
67
+ if not response.content:
68
+ return {}
69
+ try:
70
+ decoded = response.json()
71
+ except ValueError as exc:
72
+ raise RuntimeAPIError("server returned invalid JSON", status_code=response.status_code) from exc
73
+
74
+ if not isinstance(decoded, dict):
75
+ raise RuntimeAPIError("server returned non-object JSON", status_code=response.status_code)
76
+ return decoded
runtime_sdk/config.py ADDED
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ DEFAULT_BASE_URL = "http://127.0.0.1:8080"
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class PendingSignup:
14
+ flow_id: int
15
+ email: str
16
+ expires_at: str | None = None
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class RuntimeConfig:
21
+ base_url: str = DEFAULT_BASE_URL
22
+ api_key: str | None = None
23
+ pending_signup: PendingSignup | None = None
24
+
25
+
26
+ def config_path() -> Path:
27
+ root = os.environ.get("XDG_CONFIG_HOME")
28
+ if root:
29
+ return Path(root) / "runtime" / "config.json"
30
+ return Path.home() / ".config" / "runtime" / "config.json"
31
+
32
+
33
+ def load_config() -> RuntimeConfig:
34
+ path = config_path()
35
+ if not path.exists():
36
+ return RuntimeConfig(base_url=os.environ.get("RUNTIME_BASE_URL", DEFAULT_BASE_URL))
37
+
38
+ data = json.loads(path.read_text())
39
+ pending = data.get("pending_signup")
40
+ return RuntimeConfig(
41
+ base_url=data.get("base_url") or os.environ.get("RUNTIME_BASE_URL", DEFAULT_BASE_URL),
42
+ api_key=data.get("api_key"),
43
+ pending_signup=PendingSignup(**pending) if pending else None,
44
+ )
45
+
46
+
47
+ def save_config(config: RuntimeConfig) -> None:
48
+ path = config_path()
49
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
50
+ payload = asdict(config)
51
+ path.write_text(json.dumps(payload, indent=2) + "\n")
52
+ os.chmod(path, 0o600)
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: runtime-sdk
3
+ Version: 0.1.0
4
+ Summary: Runtime Python SDK and CLI
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.28.1
8
+
9
+ # runtime-sdk
10
+
11
+ Python SDK and CLI for Runtime.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ uv tool install runtime-sdk
17
+ ```
18
+
19
+ ## Configure
20
+
21
+ The CLI talks to `http://127.0.0.1:8080` by default. Override it with:
22
+
23
+ ```bash
24
+ export RUNTIME_BASE_URL=https://runruntime.dev
25
+ ```
26
+
27
+ Or pass `--base-url` per command.
28
+
29
+ ## Usage
30
+
31
+ ```bash
32
+ runtime signup you@example.com --name "Your Name"
33
+ runtime verify 123456
34
+ runtime whoami
35
+ runtime logout
36
+ ```
37
+
38
+ ## Python
39
+
40
+ ```python
41
+ from runtime_sdk import RuntimeClient
42
+
43
+ client = RuntimeClient(base_url="https://runruntime.dev")
44
+ result = client.signup("you@example.com", "Your Name")
45
+ print(result["flow_id"])
46
+ ```
@@ -0,0 +1,9 @@
1
+ runtime_sdk/__init__.py,sha256=Chc9jwyjuIjLAO6LMosodvmyN-sOxhXlGCVaT-LfBto,110
2
+ runtime_sdk/cli.py,sha256=I3I9QSvUE2-7PGGTQDT5Gezs0TtzSd3Ey1lvJ7tO7n4,3978
3
+ runtime_sdk/client.py,sha256=uPOAt35L7miX0dqm2UKAM-uRcrugb6uDvtlGYeWNtyU,2577
4
+ runtime_sdk/config.py,sha256=KTUFv1s4vi1KDogVEVzLaDgzasBk0Mhm_AWD4x3WDHs,1396
5
+ runtime_sdk-0.1.0.dist-info/METADATA,sha256=xrubKwPysdsqMTg16jmnb73w-kbMva4bab0kxqr5Png,794
6
+ runtime_sdk-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ runtime_sdk-0.1.0.dist-info/entry_points.txt,sha256=Hk_zo0mQTeqWOYb1oTxGnGN-b61_6uKYAt3THED8xeE,49
8
+ runtime_sdk-0.1.0.dist-info/top_level.txt,sha256=RmTYN3o3Mn1j6OozW1LbYaYvy3FjQU3E118nfNv7Ye0,12
9
+ runtime_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ runtime = runtime_sdk.cli:main
@@ -0,0 +1 @@
1
+ runtime_sdk