unikraft-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.
Files changed (44) hide show
  1. unikraft_cloud-0.1.0/LICENSE.md +28 -0
  2. unikraft_cloud-0.1.0/PKG-INFO +352 -0
  3. unikraft_cloud-0.1.0/README.md +331 -0
  4. unikraft_cloud-0.1.0/pyproject.toml +64 -0
  5. unikraft_cloud-0.1.0/src/unikraft_cloud/__init__.py +206 -0
  6. unikraft_cloud-0.1.0/src/unikraft_cloud/api/__init__.py +53 -0
  7. unikraft_cloud-0.1.0/src/unikraft_cloud/api/controlplane/__init__.py +51 -0
  8. unikraft_cloud-0.1.0/src/unikraft_cloud/api/controlplane/auth_gen.py +72 -0
  9. unikraft_cloud-0.1.0/src/unikraft_cloud/api/controlplane/images_gen.py +40 -0
  10. unikraft_cloud-0.1.0/src/unikraft_cloud/api/controlplane/metros_gen.py +34 -0
  11. unikraft_cloud-0.1.0/src/unikraft_cloud/api/controlplane/models_gen.py +1006 -0
  12. unikraft_cloud-0.1.0/src/unikraft_cloud/api/controlplane/node_activation_service_gen.py +40 -0
  13. unikraft_cloud-0.1.0/src/unikraft_cloud/api/controlplane/node_service_gen.py +271 -0
  14. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/__init__.py +70 -0
  15. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/autoscale_gen.py +233 -0
  16. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/certificates_gen.py +164 -0
  17. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/images_gen.py +67 -0
  18. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/instances_gen.py +816 -0
  19. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/models_gen.py +6964 -0
  20. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/node_gen.py +34 -0
  21. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/service_groups_gen.py +168 -0
  22. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/users_gen.py +72 -0
  23. unikraft_cloud-0.1.0/src/unikraft_cloud/api/platform/volumes_gen.py +439 -0
  24. unikraft_cloud-0.1.0/src/unikraft_cloud/client.py +270 -0
  25. unikraft_cloud-0.1.0/src/unikraft_cloud/core/__init__.py +1 -0
  26. unikraft_cloud-0.1.0/src/unikraft_cloud/core/errors.py +208 -0
  27. unikraft_cloud-0.1.0/src/unikraft_cloud/core/fanout.py +263 -0
  28. unikraft_cloud-0.1.0/src/unikraft_cloud/core/handle.py +204 -0
  29. unikraft_cloud-0.1.0/src/unikraft_cloud/core/handle_set.py +112 -0
  30. unikraft_cloud-0.1.0/src/unikraft_cloud/core/http.py +553 -0
  31. unikraft_cloud-0.1.0/src/unikraft_cloud/core/metro.py +141 -0
  32. unikraft_cloud-0.1.0/src/unikraft_cloud/core/pagination.py +159 -0
  33. unikraft_cloud-0.1.0/src/unikraft_cloud/core/patch.py +168 -0
  34. unikraft_cloud-0.1.0/src/unikraft_cloud/core/resource.py +458 -0
  35. unikraft_cloud-0.1.0/src/unikraft_cloud/core/response.py +300 -0
  36. unikraft_cloud-0.1.0/src/unikraft_cloud/core/session.py +218 -0
  37. unikraft_cloud-0.1.0/src/unikraft_cloud/py.typed +0 -0
  38. unikraft_cloud-0.1.0/src/unikraft_cloud/resources/__init__.py +1 -0
  39. unikraft_cloud-0.1.0/src/unikraft_cloud/resources/_shared.py +139 -0
  40. unikraft_cloud-0.1.0/src/unikraft_cloud/resources/certificates.py +350 -0
  41. unikraft_cloud-0.1.0/src/unikraft_cloud/resources/instances.py +861 -0
  42. unikraft_cloud-0.1.0/src/unikraft_cloud/resources/service_groups.py +401 -0
  43. unikraft_cloud-0.1.0/src/unikraft_cloud/resources/users.py +63 -0
  44. unikraft_cloud-0.1.0/src/unikraft_cloud/resources/volumes.py +500 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Unikraft GmbH. All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,352 @@
1
+ Metadata-Version: 2.4
2
+ Name: unikraft-cloud
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Unikraft Cloud Platform API
5
+ Keywords: unikraft,unikraft-cloud,kraftcloud,sdk,unikernel,cloud,api
6
+ Author: Unikraft GmbH
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE.md
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Typing :: Typed
14
+ Requires-Dist: httpx>=0.27
15
+ Requires-Dist: pydantic>=2.7
16
+ Requires-Python: >=3.10
17
+ Project-URL: Homepage, https://unikraft.com
18
+ Project-URL: Issues, https://github.com/unikraft-cloud/python-sdk/issues
19
+ Project-URL: Repository, https://github.com/unikraft-cloud/python-sdk
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Unikraft Cloud Python SDK
23
+
24
+ The official Python SDK for the [Unikraft Cloud](https://unikraft.com) Platform and
25
+ control-plane APIs.
26
+
27
+ It has two layers. The **idiomatic** layer is what you reach for: envelope-free results,
28
+ automatic pagination, chainable references, and multi-metro fan-out. The **plumbing**
29
+ layer underneath mirrors the OpenAPI specification exactly, and stays available for
30
+ anything the idiomatic layer does not cover yet.
31
+
32
+ The SDK is async-only.
33
+
34
+ ## Installation
35
+
36
+ ```sh
37
+ pip install unikraft-cloud
38
+ ```
39
+
40
+ Requires Python 3.10 or newer.
41
+
42
+ ## Quickstart
43
+
44
+ ```python
45
+ import asyncio
46
+
47
+ from unikraft_cloud import UnikraftCloud
48
+
49
+
50
+ async def main() -> None:
51
+ async with UnikraftCloud() as ukc: # token from UKC_TOKEN
52
+ instance = await ukc.metro("fra").instances.create(
53
+ image="nginx:latest", memory_mb=256, autostart=True
54
+ )
55
+ print(instance.name, instance.uuid, instance.metro)
56
+
57
+
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ The client owns a connection pool, so close it when you are done — either with
62
+ `async with`, or by awaiting `ukc.aclose()`.
63
+
64
+ ## Configuration
65
+
66
+ ```python
67
+ ukc = UnikraftCloud(
68
+ token="...", # falls back to UKC_TOKEN
69
+ metro="fra", # falls back to UKC_METRO; omit to cover every metro
70
+ )
71
+ ```
72
+
73
+ | Argument | Purpose |
74
+ | --- | --- |
75
+ | `token` | Bearer token. Falls back to `UKC_TOKEN`. |
76
+ | `metro` | The metro operations default to, or a full `http(s)://` URL for a staging or self-hosted deployment. Falls back to `UKC_METRO`. A code leaves the other metros reachable with `ukc.metro(...)`; a URL pins the client to that endpoint, and naming another metro then raises. |
77
+ | `metros` | The metros operations cover by default: `"all"`, one metro, or a list. Creating a resource needs exactly one, so name a metro somewhere when you create. |
78
+ | `base_url` | Explicit platform API base URL. It settles where requests go, so it overrides `metro` and `UKC_METRO` alike, and pins the client to that one endpoint. |
79
+ | `control_plane_url` | Override the control-plane API base URL. |
80
+ | `headers` | Extra headers sent with every request. |
81
+ | `user_agent` | Override the default User-Agent. |
82
+ | `http` | An `httpx.AsyncClient` to send through. Supplying one makes its lifetime yours. |
83
+ | `transport` | An `httpx.AsyncBaseTransport`, chiefly for testing with `httpx.MockTransport`. |
84
+ | `trust_env` | Honour `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`. Defaults to `True`. |
85
+ | `timeout` | Timeout for every request. Omitted, an injected `http` client keeps its own; otherwise the default bounds connecting but not reading, because `wait` operations block for as long as you asked. |
86
+
87
+ ## Metros
88
+
89
+ The platform API is metro-scoped. By default the client is **account-wide**: reads ask
90
+ every metro the account can reach and merge the answers as they arrive, and each result
91
+ carries the metro it came from.
92
+
93
+ ```python
94
+ # Every metro, merged as the pages arrive. Await the listing instead for a list.
95
+ async for inst in ukc.instances.list(details=True):
96
+ print(inst.metro, inst.name, inst.state)
97
+ every = await ukc.instances.list(details=True) # one pass each: call list() again for more
98
+
99
+ # One metro. Because it is known, no lookup is needed.
100
+ await ukc.metro("fra").instances.get(name="web").suspend()
101
+
102
+ # Several metros, for one call or for a whole client.
103
+ async for inst in ukc.instances.list(metros=["fra", "dal"]):
104
+ ...
105
+ scoped = ukc.metros(["fra", "dal"])
106
+
107
+ # A listing you stop reading holds a page of every metro, so close it.
108
+ async with ukc.instances.list() as listing:
109
+ async for inst in listing:
110
+ break
111
+
112
+ # What the account can reach, as the control plane reports it.
113
+ for endpoint in await ukc.available_metros():
114
+ print(endpoint.metro, endpoint.base_url)
115
+ ```
116
+
117
+ Naming metros is also how you skip metro discovery, which is otherwise one extra request
118
+ per client.
119
+
120
+ ## References
121
+
122
+ A resource is addressed by `name` or `uuid` — one or the other, because the API validates
123
+ whichever field it is given.
124
+
125
+ ```python
126
+ await ukc.instances.get(name="web")
127
+ await ukc.instances.get(uuid="550e8400-e29b-41d4-a716-446655440000")
128
+ ```
129
+
130
+ A name is only unique **within** a metro, so the same name can exist in several. Add
131
+ `metro=` to say which you mean, which also saves a lookup:
132
+
133
+ ```python
134
+ await ukc.instances.get(name="web", metro="fra")
135
+ ```
136
+
137
+ Without it, and with more than one metro in scope, the SDK asks every metro. If the name
138
+ matches in several it raises `AmbiguousRefError` rather than picking one — with the
139
+ matches attached, so recovering costs no further requests:
140
+
141
+ ```python
142
+ from unikraft_cloud import AmbiguousRefError
143
+
144
+ try:
145
+ await ukc.instances.get(name="web")
146
+ except AmbiguousRefError as err:
147
+ print(err.metros) # ("fra", "dal")
148
+ print([m.uuid for m in err.matches])
149
+ ```
150
+
151
+ To act on all of them deliberately, use `each()`:
152
+
153
+ ```python
154
+ await ukc.instances.each(name="web").suspend() # in every metro that has one
155
+ ```
156
+
157
+ Bulk operations take a sequence of references, as `Ref` objects, plain dicts, or names:
158
+
159
+ ```python
160
+ from unikraft_cloud import Ref
161
+
162
+ await ukc.instances.delete([Ref(uuid="a"), {"name": "b"}, "web"])
163
+ ```
164
+
165
+ An operation the API could only carry out in part raises, naming what failed. What did
166
+ succeed is on `err.results`, so a partial failure costs nothing already done:
167
+
168
+ ```python
169
+ from unikraft_cloud import NotFoundError
170
+
171
+ try:
172
+ await ukc.instances.delete(["web", "gone"])
173
+ except NotFoundError as err:
174
+ print([deleted.name for deleted in err.results]) # ["web"]
175
+ ```
176
+
177
+ ## Chainable handles
178
+
179
+ Single-resource operations return a **handle** rather than a coroutine, so they compose.
180
+ A handle is awaitable too, so awaiting one gives you the resource:
181
+
182
+ ```python
183
+ inst = await ukc.instances.get(name="web") # the instance
184
+ await ukc.instances.get(name="web").suspend() # the suspend
185
+
186
+ logs = await (
187
+ ukc.metro("fra")
188
+ .instances.create(image="nginx:latest")
189
+ .wait(state="running", timeout_seconds=30)
190
+ .logs(offset=-4096)
191
+ )
192
+ ```
193
+
194
+ Nothing is sent until a handle is awaited or an operation is chained onto it. With one
195
+ metro in scope, `get(name=...).suspend()` is a single request; when the scope spans
196
+ metros, the instance is located first so the operation reaches the metro that holds it.
197
+
198
+ A handle that is dropped without ever being awaited emits a `RuntimeWarning`: unlike a
199
+ forgotten `await` on a coroutine, nothing else would tell you no request was sent.
200
+
201
+ A handle is awaitable but is not a coroutine, so `asyncio.gather(...)` takes one while
202
+ `asyncio.create_task(...)` does not; wrap it in `asyncio.ensure_future(...)` for a task.
203
+
204
+ ## Updating
205
+
206
+ Properties are keyword arguments. A value sets the property, `REMOVE` clears it out, and
207
+ anything omitted is left alone — all in one request.
208
+
209
+ ```python
210
+ from unikraft_cloud import REMOVE
211
+
212
+ await ukc.instances.get(name="web").update(memory_mb=512, vcpus=2, autokill=REMOVE)
213
+ ```
214
+
215
+ When `set` is not what you mean — merging into a property, or removing individual members
216
+ — stage the operations and apply them together:
217
+
218
+ ```python
219
+ await (
220
+ ukc.instances.get(name="web")
221
+ .edit()
222
+ .set(memory_mb=512)
223
+ .add(env={"LOG_LEVEL": "debug"}, tags=["prod"])
224
+ .delete(env=["OLD_FLAG"])
225
+ .apply()
226
+ )
227
+ ```
228
+
229
+ `apply()` returns a handle, so the chain continues. For anything keyword arguments cannot
230
+ express, `patch()` takes the raw triples.
231
+
232
+ ## Errors
233
+
234
+ Every failure is an `UnikraftCloudError`, so one `except` catches the lot. Its `kind`
235
+ says which layer failed (`"http"`, `"network"`, `"parse"` or `"fanout"`) and `status`
236
+ carries the HTTP status where there was one.
237
+
238
+ ```python
239
+ from unikraft_cloud import NotFoundError, UnikraftCloudError
240
+
241
+ try:
242
+ await ukc.instances.get(name="web")
243
+ except NotFoundError:
244
+ ...
245
+ except UnikraftCloudError as err:
246
+ print(err.kind, err.status, err.errors)
247
+ ```
248
+
249
+ `AuthenticationError` (401/403), `NotFoundError` (404), `AlreadyExistsError` (409),
250
+ `RateLimitError` (429) and `ServerError` (5xx) are raised for the statuses they name, and
251
+ all subclass `UnikraftCloudError`. The API reports some failures inside an otherwise-200
252
+ envelope, per item; those carry the API's own code on `err.errors[n].code` and are raised
253
+ with the status that says the same thing.
254
+
255
+ A `wait()` that runs out of time raises `WaitTimeoutError`, which is also a builtin
256
+ `TimeoutError`, and carries the state the API last saw:
257
+
258
+ ```python
259
+ try:
260
+ await ukc.instances.get(name="web").wait(state="running", timeout_seconds=30)
261
+ except TimeoutError as err:
262
+ print(err.state) # e.g. "starting"
263
+ ```
264
+
265
+ When the API attaches a warning to an answer -- a deprecated field, say -- the SDK
266
+ issues it as a Python `UnikraftCloudWarning`, so the standard `warnings` filters
267
+ apply.
268
+
269
+ A multi-metro operation that only partly succeeded raises `MetroFanoutError`. An
270
+ iteration yields everything the healthy metros returned *before* raising, so a partial
271
+ failure never costs you the whole answer; operations that cannot yield as they go attach
272
+ what did arrive to `err.results`.
273
+
274
+ ```python
275
+ from unikraft_cloud import MetroFanoutError
276
+
277
+ try:
278
+ async for inst in ukc.instances.list():
279
+ ...
280
+ except MetroFanoutError as err:
281
+ print([failure.metro for failure in err.failures])
282
+ ```
283
+
284
+ ## Resources
285
+
286
+ `instances`, `volumes`, `services`, `certificates` and `users` hang off any scope —
287
+ `ukc`, `ukc.metro("fra")` or `ukc.metros([...])`.
288
+
289
+ Creating one takes the properties the API describes as keyword arguments, and a property
290
+ it does not have is a `TypeError` rather than a field the server quietly ignores.
291
+
292
+ ```python
293
+ await ukc.volumes.get(name="data").attach(to="web", at="/data")
294
+ await ukc.services.get(name="web").update(hard_limit=10)
295
+ await ukc.certificates.get(name="tls").update(chain=chain_pem, pkey=key_pem)
296
+
297
+ for quota in await ukc.users.quotas():
298
+ print(quota.metro, quota.used, quota.hard)
299
+ ```
300
+
301
+ ## The plumbing layer
302
+
303
+ Every operation in the specification is available raw, returning the response envelope
304
+ untouched. Each client talks to exactly one metro, and a single call can be redirected
305
+ with `base_url=`.
306
+
307
+ ```python
308
+ res = await ukc.api.platform.instances.get_instances(count=10)
309
+ print(res.status, res.op_time_us, res.data.instances)
310
+
311
+ await ukc.api.controlplane.metros.list_metros()
312
+
313
+ # Or per resource, alongside its idiomatic client.
314
+ await ukc.instances.api.get_instance_metrics(uuid=["..."])
315
+ ```
316
+
317
+ It can also be used on its own, without the idiomatic layer:
318
+
319
+ ```python
320
+ from unikraft_cloud import ApiClientConfig
321
+ from unikraft_cloud.api.platform import PlatformApi
322
+
323
+ config = ApiClientConfig(base_url="https://api.fra.unikraft.cloud", token=token)
324
+ async with PlatformApi(config) as api:
325
+ res = await api.instances.get_instances(count=10)
326
+ ```
327
+
328
+ ## Examples
329
+
330
+ - [`examples/quickstart.py`](examples/quickstart.py) — create, wait, read logs, list, suspend, delete
331
+ - [`examples/update.py`](examples/update.py) — patch objects and the staged editor
332
+ - [`examples/plumbing.py`](examples/plumbing.py) — the raw API on its own
333
+
334
+ ## Development
335
+
336
+ The `api/platform` and `api/controlplane` packages are generated from the OpenAPI
337
+ specification by [`openapi-gen`](https://github.com/unikraft-cloud) using the templates in
338
+ [`templates/`](templates). Everything else is hand-written. Files ending in `_gen.py` are
339
+ never edited by hand.
340
+
341
+ ```sh
342
+ make generate # regenerate both plumbing clients from the specs
343
+ make lint # ruff check + format --check
344
+ make typecheck # mypy
345
+ make test # pytest
346
+ ```
347
+
348
+ The test suite runs entirely offline through `httpx.MockTransport`.
349
+
350
+ ## Licence
351
+
352
+ BSD-3-Clause. See [`LICENSE.md`](LICENSE.md).