numinous 0.1.0__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.
- numinous-0.1.0/LICENSE +6 -0
- numinous-0.1.0/PKG-INFO +7 -0
- numinous-0.1.0/README.md +51 -0
- numinous-0.1.0/install.sh +33 -0
- numinous-0.1.0/numinous/__init__.py +17 -0
- numinous-0.1.0/numinous/cli.py +110 -0
- numinous-0.1.0/numinous/client.py +214 -0
- numinous-0.1.0/pyproject.toml +16 -0
numinous-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Apache License 2.0 — Copyright 2026 Numinous
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
4
|
+
use this software except in compliance with the License. You may obtain a
|
|
5
|
+
copy at http://www.apache.org/licenses/LICENSE-2.0. Distributed on an
|
|
6
|
+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
|
numinous-0.1.0/PKG-INFO
ADDED
numinous-0.1.0/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# numinous
|
|
2
|
+
|
|
3
|
+
Python SDK for [Numinous Cloud](https://cloud.numinous.technology) — build &
|
|
4
|
+
test environments with hard TTLs, typed failure causes,
|
|
5
|
+
export-that-survives-teardown, and per-second metering. Everything
|
|
6
|
+
non-preemptible.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
curl -fsSL https://cloud.numinous.technology/install.sh | sh
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Installs the `numinous` CLI + Python SDK (via pipx/uv/pip, whichever exists).
|
|
15
|
+
|
|
16
|
+
## Use
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from numinous import Numinous
|
|
20
|
+
|
|
21
|
+
nc = Numinous() # NUMINOUS_API_URL / NUMINOUS_API_KEY from env
|
|
22
|
+
|
|
23
|
+
tpl = nc.templates.pack("build-env", image="ubuntu:24.04",
|
|
24
|
+
warm_cmd="apt-get update && apt-get install -y build-essential")
|
|
25
|
+
|
|
26
|
+
sb = nc.sandboxes.create(template_id=tpl["id"], vcpu=4, mem_gib=8,
|
|
27
|
+
ttl_seconds=7200, launch_token="job-1-attempt-1",
|
|
28
|
+
labels={"trial_id": "tr_1"})
|
|
29
|
+
|
|
30
|
+
nc.sandboxes.exec(sb["id"], "make -j test")
|
|
31
|
+
nc.sandboxes.export(sb["id"], "/logs", to="s3://bucket/tr_1/") # works after death too
|
|
32
|
+
out = nc.sandboxes.destroy(sb["id"])
|
|
33
|
+
out["teardown_proof"] # {"verified_absent": true, ...}
|
|
34
|
+
nc.usage.query(label="trial_id:tr_1") # per-second spans, typed unbilled faults
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Typed errors
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from numinous import Numinous, NuminousError
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
nc.sandboxes.create(...)
|
|
44
|
+
except NuminousError as e:
|
|
45
|
+
e.cause # "provider_capacity" | "user_image_build_failed" | ...
|
|
46
|
+
e.is_provider_fault # provider_* causes are never billed
|
|
47
|
+
e.retryable
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
API reference: the control plane serves its own OpenAPI spec at
|
|
51
|
+
`GET /openapi.json`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Numinous Cloud CLI installer
|
|
3
|
+
# curl -fsSL https://cloud.numinous.technology/install.sh | sh
|
|
4
|
+
set -eu
|
|
5
|
+
|
|
6
|
+
REPO="https://github.com/numinous-technology/numinous-python"
|
|
7
|
+
BIN_DIR="${NUMINOUS_BIN:-$HOME/.local/bin}"
|
|
8
|
+
|
|
9
|
+
say() { printf '\033[1;34mnuminous\033[0m %s\n' "$*"; }
|
|
10
|
+
|
|
11
|
+
command -v python3 >/dev/null 2>&1 || {
|
|
12
|
+
echo "python3 is required" >&2; exit 1; }
|
|
13
|
+
|
|
14
|
+
say "installing CLI from $REPO"
|
|
15
|
+
|
|
16
|
+
if command -v pipx >/dev/null 2>&1; then
|
|
17
|
+
pipx install --force "numinous @ git+$REPO" >/dev/null
|
|
18
|
+
elif command -v uv >/dev/null 2>&1; then
|
|
19
|
+
uv tool install --force "numinous @ git+$REPO" >/dev/null
|
|
20
|
+
else
|
|
21
|
+
python3 -m pip install --user --upgrade "numinous @ git+$REPO" >/dev/null
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
mkdir -p "$BIN_DIR"
|
|
25
|
+
case ":$PATH:" in
|
|
26
|
+
*":$BIN_DIR:"*) ;;
|
|
27
|
+
*) say "add $BIN_DIR to your PATH" ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
say "installed. next:"
|
|
31
|
+
say ' export NUMINOUS_API_URL=... NUMINOUS_API_KEY=...'
|
|
32
|
+
say ' numinous capacity'
|
|
33
|
+
say ' numinous template pack --name build-env --image ubuntu:24.04'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""numinous — Python SDK for the Numinous Cloud sandbox API.
|
|
2
|
+
|
|
3
|
+
>>> from numinous import Numinous
|
|
4
|
+
>>> nc = Numinous() # NUMINOUS_API_URL / NUMINOUS_API_KEY from env
|
|
5
|
+
>>> tpl = nc.templates.pack("build-env", image="ubuntu:24.04",
|
|
6
|
+
... warm_cmd="apt-get update && apt-get install -y build-essential")
|
|
7
|
+
>>> sb = nc.sandboxes.create(template_id=tpl["id"], vcpu=4, mem_gib=8,
|
|
8
|
+
... ttl_seconds=7200, labels={"trial_id": "tr_1"})
|
|
9
|
+
>>> nc.sandboxes.exec(sb["id"], "make -j test")
|
|
10
|
+
>>> nc.sandboxes.export(sb["id"], "/logs", to="s3://bucket/tr_1/")
|
|
11
|
+
>>> nc.sandboxes.destroy(sb["id"])
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .client import Numinous, NuminousError
|
|
15
|
+
|
|
16
|
+
__all__ = ["Numinous", "NuminousError"]
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""numinous — CLI for Numinous Cloud.
|
|
2
|
+
|
|
3
|
+
Install: curl -fsSL https://cloud.numinous.technology/install.sh | sh
|
|
4
|
+
Auth: export NUMINOUS_API_URL=... NUMINOUS_API_KEY=...
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from .client import Numinous, NuminousError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _out(obj) -> None:
|
|
17
|
+
print(json.dumps(obj, indent=2, default=str))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
p = argparse.ArgumentParser(prog="numinous", description=__doc__)
|
|
22
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
23
|
+
|
|
24
|
+
tp = sub.add_parser("template", help="manage templates")
|
|
25
|
+
tps = tp.add_subparsers(dest="tcmd", required=True)
|
|
26
|
+
pack = tps.add_parser("pack", help="pack an environment into a template")
|
|
27
|
+
pack.add_argument("--name", required=True)
|
|
28
|
+
pack.add_argument("--image", help="base docker image")
|
|
29
|
+
pack.add_argument("--warm", help="command to run before snapshotting")
|
|
30
|
+
tps.add_parser("list", help="list templates")
|
|
31
|
+
|
|
32
|
+
boot = sub.add_parser("boot", help="boot sandboxes from a template")
|
|
33
|
+
boot.add_argument("template_id")
|
|
34
|
+
boot.add_argument("--vcpu", type=int, default=2)
|
|
35
|
+
boot.add_argument("--mem", type=float, default=4.0, help="GiB")
|
|
36
|
+
boot.add_argument("--ttl", type=int, default=0, help="seconds; 0=default")
|
|
37
|
+
boot.add_argument("--count", type=int, default=1)
|
|
38
|
+
boot.add_argument("--label", action="append", default=[], help="k=v")
|
|
39
|
+
boot.add_argument("--token", help="launch token (idempotency)")
|
|
40
|
+
|
|
41
|
+
ex = sub.add_parser("exec", help="run a command in a sandbox")
|
|
42
|
+
ex.add_argument("sandbox_id")
|
|
43
|
+
ex.add_argument("command")
|
|
44
|
+
ex.add_argument("--timeout", type=float, default=300)
|
|
45
|
+
|
|
46
|
+
for name in ("suspend", "resume", "destroy"):
|
|
47
|
+
c = sub.add_parser(name, help=f"{name} a sandbox")
|
|
48
|
+
c.add_argument("sandbox_id")
|
|
49
|
+
|
|
50
|
+
exp = sub.add_parser("export", help="export a path (works after death)")
|
|
51
|
+
exp.add_argument("sandbox_id")
|
|
52
|
+
exp.add_argument("path")
|
|
53
|
+
exp.add_argument("--to", help="s3://bucket/prefix")
|
|
54
|
+
|
|
55
|
+
ls = sub.add_parser("ls", help="list sandboxes")
|
|
56
|
+
ls.add_argument("--label")
|
|
57
|
+
ls.add_argument("--state")
|
|
58
|
+
|
|
59
|
+
us = sub.add_parser("usage", help="usage spans by label")
|
|
60
|
+
us.add_argument("--label")
|
|
61
|
+
|
|
62
|
+
sub.add_parser("capacity", help="free capacity")
|
|
63
|
+
sub.add_parser("pricing", help="current rates")
|
|
64
|
+
|
|
65
|
+
a = p.parse_args(argv)
|
|
66
|
+
nc = Numinous()
|
|
67
|
+
try:
|
|
68
|
+
if a.cmd == "template" and a.tcmd == "pack":
|
|
69
|
+
_out(nc.templates.pack(a.name, image=a.image, warm_cmd=a.warm))
|
|
70
|
+
elif a.cmd == "template" and a.tcmd == "list":
|
|
71
|
+
_out(nc.templates.list())
|
|
72
|
+
elif a.cmd == "boot":
|
|
73
|
+
labels = dict(kv.split("=", 1) for kv in a.label)
|
|
74
|
+
out = []
|
|
75
|
+
for i in range(a.count):
|
|
76
|
+
tok = f"{a.token}-{i}" if a.token and a.count > 1 else a.token
|
|
77
|
+
out.append(nc.sandboxes.create(
|
|
78
|
+
template_id=a.template_id, vcpu=a.vcpu, mem_gib=a.mem,
|
|
79
|
+
ttl_seconds=a.ttl, labels=labels, launch_token=tok))
|
|
80
|
+
_out(out if a.count > 1 else out[0])
|
|
81
|
+
elif a.cmd == "exec":
|
|
82
|
+
r = nc.sandboxes.exec(a.sandbox_id, a.command, timeout_sec=a.timeout)
|
|
83
|
+
sys.stdout.write(r["stdout"])
|
|
84
|
+
sys.stderr.write(r["stderr"])
|
|
85
|
+
return r["exit_code"]
|
|
86
|
+
elif a.cmd == "suspend":
|
|
87
|
+
_out(nc.sandboxes.suspend(a.sandbox_id))
|
|
88
|
+
elif a.cmd == "resume":
|
|
89
|
+
_out(nc.sandboxes.resume(a.sandbox_id))
|
|
90
|
+
elif a.cmd == "destroy":
|
|
91
|
+
_out(nc.sandboxes.destroy(a.sandbox_id))
|
|
92
|
+
elif a.cmd == "export":
|
|
93
|
+
_out(nc.sandboxes.export(a.sandbox_id, a.path, to=a.to))
|
|
94
|
+
elif a.cmd == "ls":
|
|
95
|
+
_out(nc.sandboxes.list(label=a.label, state=a.state))
|
|
96
|
+
elif a.cmd == "usage":
|
|
97
|
+
_out(nc.usage.query(label=a.label))
|
|
98
|
+
elif a.cmd == "capacity":
|
|
99
|
+
_out(nc.capacity.get())
|
|
100
|
+
elif a.cmd == "pricing":
|
|
101
|
+
_out(nc.pricing())
|
|
102
|
+
return 0
|
|
103
|
+
except NuminousError as e:
|
|
104
|
+
print(f"error [{e.cause}]: {e.message}", file=sys.stderr)
|
|
105
|
+
# provider faults are retryable and unbilled; exit codes reflect class
|
|
106
|
+
return 75 if e.is_provider_fault else 1
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NuminousError(RuntimeError):
|
|
11
|
+
"""API error carrying the typed cause.
|
|
12
|
+
|
|
13
|
+
err.cause is one of: user_image_build_failed, user_oom, user_timeout,
|
|
14
|
+
provider_capacity, provider_infra, policy_killed_ttl, policy_killed_quota,
|
|
15
|
+
auth, state, not_found.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, cause: str, message: str, status: int):
|
|
19
|
+
super().__init__(f"[{cause}] {message}")
|
|
20
|
+
self.cause = cause
|
|
21
|
+
self.message = message
|
|
22
|
+
self.status = status
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def is_provider_fault(self) -> bool:
|
|
26
|
+
return self.cause.startswith("provider_")
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def retryable(self) -> bool:
|
|
30
|
+
# capacity clears (or use reservations); infra may clear; user_* will not.
|
|
31
|
+
return self.cause.startswith("provider_")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _Resource:
|
|
35
|
+
def __init__(self, c: "Numinous"):
|
|
36
|
+
self._c = c
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Templates(_Resource):
|
|
40
|
+
def pack(self, name: str, *, image: str | None = None,
|
|
41
|
+
dockerfile: str | None = None, context: str | None = None,
|
|
42
|
+
warm_cmd: str | None = None) -> dict:
|
|
43
|
+
source: dict[str, Any] = {}
|
|
44
|
+
if image:
|
|
45
|
+
source = {"type": "image", "image": image, "warm_cmd": warm_cmd}
|
|
46
|
+
elif dockerfile:
|
|
47
|
+
source = {"type": "dockerfile", "dockerfile": dockerfile,
|
|
48
|
+
"context": context or ".", "warm_cmd": warm_cmd}
|
|
49
|
+
return self._c._post("/v1/templates", {"name": name, "source": source},
|
|
50
|
+
timeout=1800)
|
|
51
|
+
|
|
52
|
+
def list(self) -> list[dict]:
|
|
53
|
+
return self._c._get("/v1/templates")
|
|
54
|
+
|
|
55
|
+
def get(self, template_id: str) -> dict:
|
|
56
|
+
return self._c._get(f"/v1/templates/{template_id}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Roms(_Resource):
|
|
60
|
+
def create(self, name: str, files: dict[str, str]) -> dict:
|
|
61
|
+
return self._c._post("/v1/roms", {"name": name, "files": files})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Sandboxes(_Resource):
|
|
65
|
+
def create(self, *, template_id: str | None = None, image: str | None = None,
|
|
66
|
+
rom_id: str | None = None, vcpu: int = 2, mem_gib: float = 4.0,
|
|
67
|
+
ttl_seconds: int = 0, launch_token: str | None = None,
|
|
68
|
+
labels: dict[str, str] | None = None,
|
|
69
|
+
egress: str = "allow", allow: list[str] | None = None,
|
|
70
|
+
env: dict[str, str] | None = None) -> dict:
|
|
71
|
+
return self._c._post("/v1/sandboxes", {
|
|
72
|
+
"template_id": template_id, "image": image, "rom_id": rom_id,
|
|
73
|
+
"vcpu": vcpu, "mem_gib": mem_gib, "ttl_seconds": ttl_seconds,
|
|
74
|
+
"launch_token": launch_token, "labels": labels or {},
|
|
75
|
+
"network": {"egress": egress, "allow": allow or []},
|
|
76
|
+
"env": env or {},
|
|
77
|
+
}, timeout=600)
|
|
78
|
+
|
|
79
|
+
def get(self, sandbox_id: str) -> dict:
|
|
80
|
+
return self._c._get(f"/v1/sandboxes/{sandbox_id}")
|
|
81
|
+
|
|
82
|
+
def list(self, *, label: str | None = None, state: str | None = None) -> list[dict]:
|
|
83
|
+
params = {}
|
|
84
|
+
if label:
|
|
85
|
+
params["label"] = label
|
|
86
|
+
if state:
|
|
87
|
+
params["state"] = state
|
|
88
|
+
return self._c._get("/v1/sandboxes", params=params)
|
|
89
|
+
|
|
90
|
+
def exec(self, sandbox_id: str, command: str, timeout_sec: float = 300,
|
|
91
|
+
*, cwd: str | None = None, env: dict[str, str] | None = None,
|
|
92
|
+
user: str | None = None) -> dict:
|
|
93
|
+
return self._c._post(f"/v1/sandboxes/{sandbox_id}/exec",
|
|
94
|
+
{"command": command, "timeout_sec": timeout_sec,
|
|
95
|
+
"cwd": cwd, "env": env or {}, "user": user},
|
|
96
|
+
timeout=timeout_sec + 30)
|
|
97
|
+
|
|
98
|
+
def logs(self, sandbox_id: str, tail: int = 500) -> str:
|
|
99
|
+
return self._c._get(f"/v1/sandboxes/{sandbox_id}/logs",
|
|
100
|
+
params={"tail": tail})["logs"]
|
|
101
|
+
|
|
102
|
+
def suspend(self, sandbox_id: str) -> dict:
|
|
103
|
+
return self._c._post(f"/v1/sandboxes/{sandbox_id}/suspend", {})
|
|
104
|
+
|
|
105
|
+
def resume(self, sandbox_id: str) -> dict:
|
|
106
|
+
return self._c._post(f"/v1/sandboxes/{sandbox_id}/resume", {})
|
|
107
|
+
|
|
108
|
+
def export(self, sandbox_id: str, path: str, to: str | None = None) -> dict:
|
|
109
|
+
"""Works during the run and after the sandbox terminated."""
|
|
110
|
+
return self._c._post(f"/v1/sandboxes/{sandbox_id}/export",
|
|
111
|
+
{"path": path, "to": to}, timeout=1800)
|
|
112
|
+
|
|
113
|
+
def destroy(self, sandbox_id: str) -> dict:
|
|
114
|
+
"""Returns the sandbox with teardown_proof attached."""
|
|
115
|
+
return self._c._delete(f"/v1/sandboxes/{sandbox_id}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def put_file(self, sandbox_id: str, path: str, data: bytes) -> dict:
|
|
119
|
+
import base64
|
|
120
|
+
return self._c._put(f"/v1/sandboxes/{sandbox_id}/files",
|
|
121
|
+
{"path": path,
|
|
122
|
+
"content_b64": base64.b64encode(data).decode()})
|
|
123
|
+
|
|
124
|
+
def get_file(self, sandbox_id: str, path: str) -> bytes:
|
|
125
|
+
import base64
|
|
126
|
+
out = self._c._get(f"/v1/sandboxes/{sandbox_id}/files",
|
|
127
|
+
params={"path": path})
|
|
128
|
+
return base64.b64decode(out["content_b64"])
|
|
129
|
+
|
|
130
|
+
def events(self, sandbox_id: str) -> list[dict]:
|
|
131
|
+
return self._c._get(f"/v1/sandboxes/{sandbox_id}/events")
|
|
132
|
+
|
|
133
|
+
def wait(self, sandbox_id: str, *, until: str = "terminated",
|
|
134
|
+
timeout: float = 600, poll: float = 2.0) -> dict:
|
|
135
|
+
deadline = time.monotonic() + timeout
|
|
136
|
+
while time.monotonic() < deadline:
|
|
137
|
+
sb = self.get(sandbox_id)
|
|
138
|
+
if sb["state"] in (until, "failed", "terminated"):
|
|
139
|
+
return sb
|
|
140
|
+
time.sleep(poll)
|
|
141
|
+
raise TimeoutError(f"{sandbox_id} not {until} after {timeout}s")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Usage(_Resource):
|
|
145
|
+
def query(self, *, label: str | None = None) -> dict:
|
|
146
|
+
params = {"label": label} if label else {}
|
|
147
|
+
return self._c._get("/v1/usage", params=params)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Capacity(_Resource):
|
|
151
|
+
def get(self) -> dict:
|
|
152
|
+
return self._c._get("/v1/capacity")
|
|
153
|
+
|
|
154
|
+
def reserve(self, *, vcpu: int, mem_gib: float, count: int,
|
|
155
|
+
duration_minutes: int = 120,
|
|
156
|
+
labels: dict[str, str] | None = None) -> dict:
|
|
157
|
+
return self._c._post("/v1/reservations", {
|
|
158
|
+
"vcpu": vcpu, "mem_gib": mem_gib, "count": count,
|
|
159
|
+
"duration_minutes": duration_minutes, "labels": labels or {}})
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Numinous:
|
|
163
|
+
def __init__(self, api_url: str | None = None, api_key: str | None = None):
|
|
164
|
+
self.api_url = (api_url or os.environ.get(
|
|
165
|
+
"NUMINOUS_API_URL", "http://127.0.0.1:8400")).rstrip("/")
|
|
166
|
+
self.api_key = api_key or os.environ.get("NUMINOUS_API_KEY", "nk_local_dev")
|
|
167
|
+
self._http = httpx.Client(
|
|
168
|
+
base_url=self.api_url,
|
|
169
|
+
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
170
|
+
timeout=60,
|
|
171
|
+
)
|
|
172
|
+
self.templates = Templates(self)
|
|
173
|
+
self.roms = Roms(self)
|
|
174
|
+
self.sandboxes = Sandboxes(self)
|
|
175
|
+
self.usage = Usage(self)
|
|
176
|
+
self.capacity = Capacity(self)
|
|
177
|
+
|
|
178
|
+
def pricing(self) -> dict:
|
|
179
|
+
return self._get("/v1/pricing")
|
|
180
|
+
|
|
181
|
+
def healthz(self) -> dict:
|
|
182
|
+
return self._get("/v1/healthz")
|
|
183
|
+
|
|
184
|
+
# -- transport ----------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def _raise_for(self, r: httpx.Response) -> None:
|
|
187
|
+
if r.status_code < 400:
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
detail = r.json().get("detail", {})
|
|
191
|
+
except Exception:
|
|
192
|
+
detail = {}
|
|
193
|
+
raise NuminousError(detail.get("cause", "unknown"),
|
|
194
|
+
detail.get("message", r.text[:300]), r.status_code)
|
|
195
|
+
|
|
196
|
+
def _get(self, path: str, params: dict | None = None) -> Any:
|
|
197
|
+
r = self._http.get(path, params=params)
|
|
198
|
+
self._raise_for(r)
|
|
199
|
+
return r.json()
|
|
200
|
+
|
|
201
|
+
def _post(self, path: str, body: dict, timeout: float = 120) -> Any:
|
|
202
|
+
r = self._http.post(path, json=body, timeout=timeout)
|
|
203
|
+
self._raise_for(r)
|
|
204
|
+
return r.json()
|
|
205
|
+
|
|
206
|
+
def _put(self, path: str, body: dict, timeout: float = 600) -> Any:
|
|
207
|
+
r = self._http.put(path, json=body, timeout=timeout)
|
|
208
|
+
self._raise_for(r)
|
|
209
|
+
return r.json()
|
|
210
|
+
|
|
211
|
+
def _delete(self, path: str) -> Any:
|
|
212
|
+
r = self._http.delete(path)
|
|
213
|
+
self._raise_for(r)
|
|
214
|
+
return r.json()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "numinous"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for Numinous Cloud sandboxes"
|
|
5
|
+
requires-python = ">=3.9"
|
|
6
|
+
dependencies = ["httpx>=0.24"]
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
numinous = "numinous.cli:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["numinous"]
|