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.
@@ -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"]