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.
- runtime_sdk/__init__.py +3 -0
- runtime_sdk/cli.py +121 -0
- runtime_sdk/client.py +76 -0
- runtime_sdk/config.py +52 -0
- runtime_sdk-0.1.0.dist-info/METADATA +46 -0
- runtime_sdk-0.1.0.dist-info/RECORD +9 -0
- runtime_sdk-0.1.0.dist-info/WHEEL +5 -0
- runtime_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- runtime_sdk-0.1.0.dist-info/top_level.txt +1 -0
runtime_sdk/__init__.py
ADDED
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 @@
|
|
|
1
|
+
runtime_sdk
|