chassis-cloud 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.
- chassis_cloud-0.1.0/.gitignore +23 -0
- chassis_cloud-0.1.0/PKG-INFO +167 -0
- chassis_cloud-0.1.0/README.md +150 -0
- chassis_cloud-0.1.0/chassis/__init__.py +235 -0
- chassis_cloud-0.1.0/pyproject.toml +30 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
.DS_Store
|
|
3
|
+
dist
|
|
4
|
+
dist-ssr
|
|
5
|
+
*.local
|
|
6
|
+
.env
|
|
7
|
+
.env.local
|
|
8
|
+
*.pem
|
|
9
|
+
sdk/dist
|
|
10
|
+
.nitro
|
|
11
|
+
.tanstack
|
|
12
|
+
.wrangler
|
|
13
|
+
.output
|
|
14
|
+
.vinxi
|
|
15
|
+
__unconfig*
|
|
16
|
+
todos.json
|
|
17
|
+
*-resp.json
|
|
18
|
+
tsc-scoped.txt
|
|
19
|
+
scripts/_git-status.txt
|
|
20
|
+
scripts/_npm-install.log
|
|
21
|
+
|
|
22
|
+
.npmrc
|
|
23
|
+
**/.npmrc
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chassis-cloud
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Chassis GPU cloud API by OkeyMeta Ltd
|
|
5
|
+
Project-URL: Homepage, https://chassis.okeymeta.com.ng/docs
|
|
6
|
+
Project-URL: Documentation, https://chassis.okeymeta.com.ng/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/okeymeta/chassis
|
|
8
|
+
Project-URL: Issues, https://github.com/okeymeta/chassis/issues
|
|
9
|
+
Author-email: Okechukwu Nwaozor <okechukwunwaozor9@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: africa,ai,chassis,cloud,gpu,okeymeta,sdk
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Provides-Extra: requests
|
|
15
|
+
Requires-Dist: requests>=2.31.0; extra == 'requests'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# chassis (Python)
|
|
19
|
+
|
|
20
|
+
Official Python client for the Chassis public API (`/api/v1`).
|
|
21
|
+
|
|
22
|
+
Default base URL: `https://chassis.okeymeta.com.ng/api/v1`
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install chassis-cloud
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
From this monorepo during development:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e packages/sdk-python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires **Python 3.10+** and **httpx**.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from chassis import Chassis
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Auth
|
|
43
|
+
|
|
44
|
+
Create an API key in the Chassis console under **API Keys**. Keys look like `chs_…` and are shown **once**.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from chassis import Chassis
|
|
48
|
+
|
|
49
|
+
with Chassis(api_key="chs_...") as client:
|
|
50
|
+
...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Optional local override:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
Chassis(api_key="chs_...", base_url="http://localhost:3000/api/v1")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from chassis import Chassis
|
|
63
|
+
|
|
64
|
+
with Chassis(api_key="chs_...") as client:
|
|
65
|
+
gpus = client.list_gpus()
|
|
66
|
+
|
|
67
|
+
instance = client.spin_up(
|
|
68
|
+
gpuSkuId=gpus[0]["id"],
|
|
69
|
+
name="train-01",
|
|
70
|
+
gpuCount=2, # multi-GPU (1–8, SKU-capped)
|
|
71
|
+
imageName="pytorch/pytorch:2.1.0-cuda11.8-cudnn8-devel",
|
|
72
|
+
containerDiskGb=50,
|
|
73
|
+
cloudType="SECURE",
|
|
74
|
+
ports="8888/http,22/tcp",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
client.restart(instance["id"])
|
|
78
|
+
client.stop(instance["id"])
|
|
79
|
+
client.terminate(instance["id"])
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Instance options (`create_instance` / `spin_up`)
|
|
83
|
+
|
|
84
|
+
| Field | Required | Notes |
|
|
85
|
+
|-------|----------|--------|
|
|
86
|
+
| `gpuSkuId` | yes | Chassis SKU UUID from `list_gpus()` |
|
|
87
|
+
| `name` | yes | Instance name |
|
|
88
|
+
| `gpuCount` | no | Default `1`, max `8` |
|
|
89
|
+
| `imageName` | no | Container image |
|
|
90
|
+
| `containerDiskGb` | no | Default `50` |
|
|
91
|
+
| `volumeGb` | no | Ephemeral volume GB |
|
|
92
|
+
| `networkVolumeId` | no | Chassis network volume UUID |
|
|
93
|
+
| `registryCredentialId` | no | Chassis registry credential UUID |
|
|
94
|
+
| `cloudType` | no | `"SECURE"` or `"COMMUNITY"` |
|
|
95
|
+
| `ports` | no | e.g. `"8888/http,22/tcp"` |
|
|
96
|
+
| `startAfterCreate` | no | Default `True` (`spin_up` forces `True`) |
|
|
97
|
+
|
|
98
|
+
## Methods
|
|
99
|
+
|
|
100
|
+
### GPUs & instances
|
|
101
|
+
|
|
102
|
+
| Method | HTTP |
|
|
103
|
+
|--------|------|
|
|
104
|
+
| `list_gpus()` | `GET /gpus` |
|
|
105
|
+
| `list_instances()` | `GET /instances` |
|
|
106
|
+
| `create_instance(**kwargs)` | `POST /instances` |
|
|
107
|
+
| `spin_up(**kwargs)` | `POST /instances` (`startAfterCreate: true`) |
|
|
108
|
+
| `get_instance(id)` | `GET /instances/:id` |
|
|
109
|
+
| `get_instance_logs(id, tail=None)` | `GET /instances/:id/logs` |
|
|
110
|
+
| `start(id)` | `POST /instances/:id/start` |
|
|
111
|
+
| `stop(id)` | `POST /instances/:id/stop` |
|
|
112
|
+
| `restart(id)` | `POST /instances/:id/restart` |
|
|
113
|
+
| `terminate(id)` | `DELETE /instances/:id` |
|
|
114
|
+
|
|
115
|
+
### Clusters (multi-node)
|
|
116
|
+
|
|
117
|
+
| Method | HTTP |
|
|
118
|
+
|--------|------|
|
|
119
|
+
| `list_clusters()` | `GET /clusters` |
|
|
120
|
+
| `create_cluster(**kwargs)` | `POST /clusters` |
|
|
121
|
+
| `get_cluster(id)` | `GET /clusters/:id` |
|
|
122
|
+
| `start_cluster(id)` | `POST /clusters/:id/start` |
|
|
123
|
+
| `stop_cluster(id)` | `POST /clusters/:id/stop` |
|
|
124
|
+
| `terminate_cluster(id)` | `DELETE /clusters/:id` |
|
|
125
|
+
|
|
126
|
+
Create with `name`, `gpuSkuId`, `nodeCount` (2–8), and optional `gpusPerNode`, `imageName`, `networkVolumeId`, `ports`. Nodes get `CHASSIS_CLUSTER_ID`, `CHASSIS_NODE_RANK`, and `CHASSIS_NODE_COUNT`.
|
|
127
|
+
|
|
128
|
+
### Templates, volumes, registries
|
|
129
|
+
|
|
130
|
+
| Method | HTTP |
|
|
131
|
+
|--------|------|
|
|
132
|
+
| `list_templates()` / `create_template(**kwargs)` | `/templates` |
|
|
133
|
+
| `list_volumes()` / `create_volume(**kwargs)` | `/volumes` |
|
|
134
|
+
| `list_registries()` / `create_registry(**kwargs)` | `/registries` |
|
|
135
|
+
|
|
136
|
+
### Serverless endpoints
|
|
137
|
+
|
|
138
|
+
| Method | HTTP |
|
|
139
|
+
|--------|------|
|
|
140
|
+
| `list_endpoints()` | `GET /endpoints` |
|
|
141
|
+
| `create_endpoint(**kwargs)` | `POST /endpoints` |
|
|
142
|
+
| `delete_endpoint(id)` | `DELETE /endpoints/:id` |
|
|
143
|
+
| `run(endpoint_id, body=None)` | `POST /endpoints/:id/run` |
|
|
144
|
+
| `run_sync(endpoint_id, body=None, wait_ms=None)` | `POST /endpoints/:id/runsync` |
|
|
145
|
+
|
|
146
|
+
Serverless example:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
endpoints = client.list_endpoints()
|
|
150
|
+
result = client.run_sync(
|
|
151
|
+
endpoints[0]["id"],
|
|
152
|
+
body={"input": {"prompt": "hello"}},
|
|
153
|
+
)
|
|
154
|
+
health = client.get_endpoint_health(endpoints[0]["id"])
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Create options also accept `env`, `interruptible`, and `publicIp`.
|
|
158
|
+
|
|
159
|
+
Errors raise `ChassisError` with `.status`, `.body`, and a short Chassis-safe message — never infrastructure or vendor details. Responses use Chassis public shapes only (`data` / `error`).
|
|
160
|
+
|
|
161
|
+
## Docs
|
|
162
|
+
|
|
163
|
+
Human setup guide: [https://chassis.okeymeta.com.ng/docs](https://chassis.okeymeta.com.ng/docs)
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT · OkeyMeta Ltd
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# chassis (Python)
|
|
2
|
+
|
|
3
|
+
Official Python client for the Chassis public API (`/api/v1`).
|
|
4
|
+
|
|
5
|
+
Default base URL: `https://chassis.okeymeta.com.ng/api/v1`
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install chassis-cloud
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
From this monorepo during development:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e packages/sdk-python
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires **Python 3.10+** and **httpx**.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from chassis import Chassis
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Auth
|
|
26
|
+
|
|
27
|
+
Create an API key in the Chassis console under **API Keys**. Keys look like `chs_…` and are shown **once**.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from chassis import Chassis
|
|
31
|
+
|
|
32
|
+
with Chassis(api_key="chs_...") as client:
|
|
33
|
+
...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Optional local override:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
Chassis(api_key="chs_...", base_url="http://localhost:3000/api/v1")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from chassis import Chassis
|
|
46
|
+
|
|
47
|
+
with Chassis(api_key="chs_...") as client:
|
|
48
|
+
gpus = client.list_gpus()
|
|
49
|
+
|
|
50
|
+
instance = client.spin_up(
|
|
51
|
+
gpuSkuId=gpus[0]["id"],
|
|
52
|
+
name="train-01",
|
|
53
|
+
gpuCount=2, # multi-GPU (1–8, SKU-capped)
|
|
54
|
+
imageName="pytorch/pytorch:2.1.0-cuda11.8-cudnn8-devel",
|
|
55
|
+
containerDiskGb=50,
|
|
56
|
+
cloudType="SECURE",
|
|
57
|
+
ports="8888/http,22/tcp",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
client.restart(instance["id"])
|
|
61
|
+
client.stop(instance["id"])
|
|
62
|
+
client.terminate(instance["id"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Instance options (`create_instance` / `spin_up`)
|
|
66
|
+
|
|
67
|
+
| Field | Required | Notes |
|
|
68
|
+
|-------|----------|--------|
|
|
69
|
+
| `gpuSkuId` | yes | Chassis SKU UUID from `list_gpus()` |
|
|
70
|
+
| `name` | yes | Instance name |
|
|
71
|
+
| `gpuCount` | no | Default `1`, max `8` |
|
|
72
|
+
| `imageName` | no | Container image |
|
|
73
|
+
| `containerDiskGb` | no | Default `50` |
|
|
74
|
+
| `volumeGb` | no | Ephemeral volume GB |
|
|
75
|
+
| `networkVolumeId` | no | Chassis network volume UUID |
|
|
76
|
+
| `registryCredentialId` | no | Chassis registry credential UUID |
|
|
77
|
+
| `cloudType` | no | `"SECURE"` or `"COMMUNITY"` |
|
|
78
|
+
| `ports` | no | e.g. `"8888/http,22/tcp"` |
|
|
79
|
+
| `startAfterCreate` | no | Default `True` (`spin_up` forces `True`) |
|
|
80
|
+
|
|
81
|
+
## Methods
|
|
82
|
+
|
|
83
|
+
### GPUs & instances
|
|
84
|
+
|
|
85
|
+
| Method | HTTP |
|
|
86
|
+
|--------|------|
|
|
87
|
+
| `list_gpus()` | `GET /gpus` |
|
|
88
|
+
| `list_instances()` | `GET /instances` |
|
|
89
|
+
| `create_instance(**kwargs)` | `POST /instances` |
|
|
90
|
+
| `spin_up(**kwargs)` | `POST /instances` (`startAfterCreate: true`) |
|
|
91
|
+
| `get_instance(id)` | `GET /instances/:id` |
|
|
92
|
+
| `get_instance_logs(id, tail=None)` | `GET /instances/:id/logs` |
|
|
93
|
+
| `start(id)` | `POST /instances/:id/start` |
|
|
94
|
+
| `stop(id)` | `POST /instances/:id/stop` |
|
|
95
|
+
| `restart(id)` | `POST /instances/:id/restart` |
|
|
96
|
+
| `terminate(id)` | `DELETE /instances/:id` |
|
|
97
|
+
|
|
98
|
+
### Clusters (multi-node)
|
|
99
|
+
|
|
100
|
+
| Method | HTTP |
|
|
101
|
+
|--------|------|
|
|
102
|
+
| `list_clusters()` | `GET /clusters` |
|
|
103
|
+
| `create_cluster(**kwargs)` | `POST /clusters` |
|
|
104
|
+
| `get_cluster(id)` | `GET /clusters/:id` |
|
|
105
|
+
| `start_cluster(id)` | `POST /clusters/:id/start` |
|
|
106
|
+
| `stop_cluster(id)` | `POST /clusters/:id/stop` |
|
|
107
|
+
| `terminate_cluster(id)` | `DELETE /clusters/:id` |
|
|
108
|
+
|
|
109
|
+
Create with `name`, `gpuSkuId`, `nodeCount` (2–8), and optional `gpusPerNode`, `imageName`, `networkVolumeId`, `ports`. Nodes get `CHASSIS_CLUSTER_ID`, `CHASSIS_NODE_RANK`, and `CHASSIS_NODE_COUNT`.
|
|
110
|
+
|
|
111
|
+
### Templates, volumes, registries
|
|
112
|
+
|
|
113
|
+
| Method | HTTP |
|
|
114
|
+
|--------|------|
|
|
115
|
+
| `list_templates()` / `create_template(**kwargs)` | `/templates` |
|
|
116
|
+
| `list_volumes()` / `create_volume(**kwargs)` | `/volumes` |
|
|
117
|
+
| `list_registries()` / `create_registry(**kwargs)` | `/registries` |
|
|
118
|
+
|
|
119
|
+
### Serverless endpoints
|
|
120
|
+
|
|
121
|
+
| Method | HTTP |
|
|
122
|
+
|--------|------|
|
|
123
|
+
| `list_endpoints()` | `GET /endpoints` |
|
|
124
|
+
| `create_endpoint(**kwargs)` | `POST /endpoints` |
|
|
125
|
+
| `delete_endpoint(id)` | `DELETE /endpoints/:id` |
|
|
126
|
+
| `run(endpoint_id, body=None)` | `POST /endpoints/:id/run` |
|
|
127
|
+
| `run_sync(endpoint_id, body=None, wait_ms=None)` | `POST /endpoints/:id/runsync` |
|
|
128
|
+
|
|
129
|
+
Serverless example:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
endpoints = client.list_endpoints()
|
|
133
|
+
result = client.run_sync(
|
|
134
|
+
endpoints[0]["id"],
|
|
135
|
+
body={"input": {"prompt": "hello"}},
|
|
136
|
+
)
|
|
137
|
+
health = client.get_endpoint_health(endpoints[0]["id"])
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Create options also accept `env`, `interruptible`, and `publicIp`.
|
|
141
|
+
|
|
142
|
+
Errors raise `ChassisError` with `.status`, `.body`, and a short Chassis-safe message — never infrastructure or vendor details. Responses use Chassis public shapes only (`data` / `error`).
|
|
143
|
+
|
|
144
|
+
## Docs
|
|
145
|
+
|
|
146
|
+
Human setup guide: [https://chassis.okeymeta.com.ng/docs](https://chassis.okeymeta.com.ng/docs)
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT · OkeyMeta Ltd
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
_LEAKY_ERROR = re.compile(
|
|
9
|
+
r"run\s*pod|runpod|\bupstream\b|\bgraphql\b|\bpods?\b|compute api|compute server|https?://|rest\.runpod|api\.runpod|machineid|gputypeid",
|
|
10
|
+
re.I,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
_LEAKY_KEY = re.compile(r"provider|upstream|runpod|podId|machineId|graphql", re.I)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _scrub(value: Any) -> Any:
|
|
17
|
+
if isinstance(value, str):
|
|
18
|
+
return "[redacted]" if _LEAKY_ERROR.search(value) else value
|
|
19
|
+
if isinstance(value, list):
|
|
20
|
+
return [_scrub(v) for v in value]
|
|
21
|
+
if isinstance(value, dict):
|
|
22
|
+
return {
|
|
23
|
+
k: _scrub(v)
|
|
24
|
+
for k, v in value.items()
|
|
25
|
+
if not _LEAKY_KEY.search(str(k))
|
|
26
|
+
}
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _public_error_message(message: str, status: int) -> str:
|
|
31
|
+
"""Keep SDK errors specific when safe; never surface infrastructure wording."""
|
|
32
|
+
trimmed = (message or "").strip()
|
|
33
|
+
if not trimmed or _LEAKY_ERROR.search(trimmed):
|
|
34
|
+
if status == 401:
|
|
35
|
+
return "Invalid API key."
|
|
36
|
+
if status == 403:
|
|
37
|
+
return "Insufficient API key scope."
|
|
38
|
+
if status == 404:
|
|
39
|
+
return "Resource not found."
|
|
40
|
+
if status == 429:
|
|
41
|
+
return "Too many requests. Please wait and try again."
|
|
42
|
+
return "Request failed. Please try again or contact support."
|
|
43
|
+
return trimmed if len(trimmed) <= 240 else f"{trimmed[:237]}…"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ChassisError(Exception):
|
|
47
|
+
def __init__(self, message: str, status: int, body: Any = None) -> None:
|
|
48
|
+
super().__init__(_public_error_message(message, status))
|
|
49
|
+
self.status = status
|
|
50
|
+
self.body = _scrub(body)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Chassis:
|
|
54
|
+
"""Minimal Chassis API client (httpx). Spin up GPUs from any Python runtime."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
api_key: str,
|
|
59
|
+
base_url: str = "https://chassis.okeymeta.com.ng/api/v1",
|
|
60
|
+
timeout: float = 60.0,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.api_key = api_key
|
|
63
|
+
self.base_url = base_url.rstrip("/")
|
|
64
|
+
self._client = httpx.Client(
|
|
65
|
+
base_url=self.base_url,
|
|
66
|
+
headers={
|
|
67
|
+
"Authorization": f"Bearer {api_key}",
|
|
68
|
+
"Accept": "application/json",
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
},
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
self._client.close()
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> "Chassis":
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *args: object) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
def _request(self, method: str, path: str, json: Optional[dict] = None) -> Any:
|
|
84
|
+
response = self._client.request(method, path, json=json)
|
|
85
|
+
try:
|
|
86
|
+
body = response.json()
|
|
87
|
+
except Exception:
|
|
88
|
+
body = response.text
|
|
89
|
+
|
|
90
|
+
if response.is_error:
|
|
91
|
+
if isinstance(body, dict) and isinstance(body.get("error"), str):
|
|
92
|
+
message = body["error"]
|
|
93
|
+
elif response.status_code == 401:
|
|
94
|
+
message = "Invalid API key."
|
|
95
|
+
elif response.status_code == 403:
|
|
96
|
+
message = "Insufficient API key scope."
|
|
97
|
+
else:
|
|
98
|
+
message = "Request failed. Please try again or contact support."
|
|
99
|
+
raise ChassisError(message, response.status_code, body)
|
|
100
|
+
|
|
101
|
+
return body
|
|
102
|
+
|
|
103
|
+
def list_gpus(self) -> list[dict]:
|
|
104
|
+
return self._request("GET", "/gpus").get("data", [])
|
|
105
|
+
|
|
106
|
+
def list_instances(self) -> list[dict]:
|
|
107
|
+
return self._request("GET", "/instances").get("data", [])
|
|
108
|
+
|
|
109
|
+
def create_instance(self, **kwargs: Any) -> dict:
|
|
110
|
+
return self._request("POST", "/instances", json=kwargs)["data"]
|
|
111
|
+
|
|
112
|
+
def get_instance(self, instance_id: str) -> dict:
|
|
113
|
+
return self._request("GET", f"/instances/{instance_id}")["data"]
|
|
114
|
+
|
|
115
|
+
def update_instance(self, instance_id: str, **kwargs: Any) -> dict:
|
|
116
|
+
return self._request("PATCH", f"/instances/{instance_id}", json=kwargs)[
|
|
117
|
+
"data"
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
def start(self, instance_id: str) -> dict:
|
|
121
|
+
return self._request("POST", f"/instances/{instance_id}/start")["data"]
|
|
122
|
+
|
|
123
|
+
def stop(self, instance_id: str) -> dict:
|
|
124
|
+
return self._request("POST", f"/instances/{instance_id}/stop")["data"]
|
|
125
|
+
|
|
126
|
+
def restart(self, instance_id: str) -> dict:
|
|
127
|
+
return self._request("POST", f"/instances/{instance_id}/restart")["data"]
|
|
128
|
+
|
|
129
|
+
def spin_up(self, **kwargs: Any) -> dict:
|
|
130
|
+
"""Create and start an instance in one call (from notebooks, CI, or services)."""
|
|
131
|
+
payload = {**kwargs, "startAfterCreate": True}
|
|
132
|
+
return self.create_instance(**payload)
|
|
133
|
+
|
|
134
|
+
def terminate(self, instance_id: str) -> dict:
|
|
135
|
+
return self._request("DELETE", f"/instances/{instance_id}")["data"]
|
|
136
|
+
|
|
137
|
+
def list_registries(self) -> list[dict]:
|
|
138
|
+
return self._request("GET", "/registries").get("data", [])
|
|
139
|
+
|
|
140
|
+
def create_registry(self, **kwargs: Any) -> dict:
|
|
141
|
+
return self._request("POST", "/registries", json=kwargs)["data"]
|
|
142
|
+
|
|
143
|
+
def list_templates(self) -> list[dict]:
|
|
144
|
+
return self._request("GET", "/templates").get("data", [])
|
|
145
|
+
|
|
146
|
+
def create_template(self, **kwargs: Any) -> dict:
|
|
147
|
+
return self._request("POST", "/templates", json=kwargs)["data"]
|
|
148
|
+
|
|
149
|
+
def update_template(self, template_id: str, **kwargs: Any) -> dict:
|
|
150
|
+
return self._request("PATCH", f"/templates/{template_id}", json=kwargs)[
|
|
151
|
+
"data"
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
def list_volumes(self) -> list[dict]:
|
|
155
|
+
return self._request("GET", "/volumes").get("data", [])
|
|
156
|
+
|
|
157
|
+
def create_volume(self, **kwargs: Any) -> dict:
|
|
158
|
+
return self._request("POST", "/volumes", json=kwargs)["data"]
|
|
159
|
+
|
|
160
|
+
def update_volume(self, volume_id: str, **kwargs: Any) -> dict:
|
|
161
|
+
return self._request("PATCH", f"/volumes/{volume_id}", json=kwargs)[
|
|
162
|
+
"data"
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
def list_endpoints(self) -> list[dict]:
|
|
166
|
+
return self._request("GET", "/endpoints").get("data", [])
|
|
167
|
+
|
|
168
|
+
def create_endpoint(self, **kwargs: Any) -> dict:
|
|
169
|
+
return self._request("POST", "/endpoints", json=kwargs)["data"]
|
|
170
|
+
|
|
171
|
+
def get_endpoint(self, endpoint_id: str) -> dict:
|
|
172
|
+
return self._request("GET", f"/endpoints/{endpoint_id}")["data"]
|
|
173
|
+
|
|
174
|
+
def update_endpoint(self, endpoint_id: str, **kwargs: Any) -> dict:
|
|
175
|
+
return self._request("PATCH", f"/endpoints/{endpoint_id}", json=kwargs)[
|
|
176
|
+
"data"
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
def delete_endpoint(self, endpoint_id: str) -> dict:
|
|
180
|
+
return self._request("DELETE", f"/endpoints/{endpoint_id}")["data"]
|
|
181
|
+
|
|
182
|
+
def run(self, endpoint_id: str, body: Optional[dict] = None) -> Any:
|
|
183
|
+
return self._request(
|
|
184
|
+
"POST", f"/endpoints/{endpoint_id}/run", json=body or {}
|
|
185
|
+
).get("data")
|
|
186
|
+
|
|
187
|
+
def run_sync(
|
|
188
|
+
self,
|
|
189
|
+
endpoint_id: str,
|
|
190
|
+
body: Optional[dict] = None,
|
|
191
|
+
wait_ms: Optional[int] = None,
|
|
192
|
+
) -> Any:
|
|
193
|
+
path = f"/endpoints/{endpoint_id}/runsync"
|
|
194
|
+
if wait_ms is not None:
|
|
195
|
+
path = f"{path}?wait={int(wait_ms)}"
|
|
196
|
+
return self._request("POST", path, json=body or {}).get("data")
|
|
197
|
+
|
|
198
|
+
def get_endpoint_health(self, endpoint_id: str) -> Any:
|
|
199
|
+
return self._request("GET", f"/endpoints/{endpoint_id}/health").get(
|
|
200
|
+
"data"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
def get_job(self, endpoint_id: str, job_id: str) -> Any:
|
|
204
|
+
return self._request(
|
|
205
|
+
"GET", f"/endpoints/{endpoint_id}/jobs/{job_id}"
|
|
206
|
+
).get("data")
|
|
207
|
+
|
|
208
|
+
def cancel_job(self, endpoint_id: str, job_id: str) -> Any:
|
|
209
|
+
return self._request(
|
|
210
|
+
"POST", f"/endpoints/{endpoint_id}/jobs/{job_id}/cancel"
|
|
211
|
+
).get("data")
|
|
212
|
+
|
|
213
|
+
def get_instance_logs(self, instance_id: str, tail: int | None = None) -> dict:
|
|
214
|
+
path = f"/instances/{instance_id}/logs"
|
|
215
|
+
if tail is not None:
|
|
216
|
+
path = f"{path}?tail={int(tail)}"
|
|
217
|
+
return self._request("GET", path).get("data", {})
|
|
218
|
+
|
|
219
|
+
def list_clusters(self) -> list:
|
|
220
|
+
return self._request("GET", "/clusters").get("data", [])
|
|
221
|
+
|
|
222
|
+
def create_cluster(self, **kwargs: Any) -> dict:
|
|
223
|
+
return self._request("POST", "/clusters", json=kwargs)["data"]
|
|
224
|
+
|
|
225
|
+
def get_cluster(self, cluster_id: str) -> dict:
|
|
226
|
+
return self._request("GET", f"/clusters/{cluster_id}")["data"]
|
|
227
|
+
|
|
228
|
+
def start_cluster(self, cluster_id: str) -> dict:
|
|
229
|
+
return self._request("POST", f"/clusters/{cluster_id}/start")["data"]
|
|
230
|
+
|
|
231
|
+
def stop_cluster(self, cluster_id: str) -> dict:
|
|
232
|
+
return self._request("POST", f"/clusters/{cluster_id}/stop")["data"]
|
|
233
|
+
|
|
234
|
+
def terminate_cluster(self, cluster_id: str) -> dict:
|
|
235
|
+
return self._request("DELETE", f"/clusters/{cluster_id}")["data"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "chassis-cloud"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python SDK for the Chassis GPU cloud API by OkeyMeta Ltd"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Okechukwu Nwaozor", email = "okechukwunwaozor9@gmail.com" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["chassis", "gpu", "cloud", "sdk", "okeymeta", "africa", "ai"]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://chassis.okeymeta.com.ng/docs"
|
|
18
|
+
Documentation = "https://chassis.okeymeta.com.ng/docs"
|
|
19
|
+
Repository = "https://github.com/okeymeta/chassis"
|
|
20
|
+
Issues = "https://github.com/okeymeta/chassis/issues"
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
requests = ["requests>=2.31.0"]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["chassis"]
|