drukbox-python-sdk 0.2.0__py3-none-any.whl → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: drukbox-python-sdk
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Async Python client for Drukbox.
5
5
  Project-URL: Homepage, https://github.com/clawhaven/drukbox-python-sdk
6
6
  Project-URL: Repository, https://github.com/clawhaven/drukbox-python-sdk
@@ -59,6 +59,8 @@ finally:
59
59
  few minutes worst case. The SDK's default `timeout` (300s) covers this. Pass
60
60
  an `idempotency_key` for retry safety: a retry with the same key after a
61
61
  successful provision returns the original host instead of creating a duplicate.
62
+ A provider at its host limit raises `SandboxCapacityError`, a
63
+ `SandboxUnavailableError` whose `retry_after` gives the seconds to wait.
62
64
 
63
65
  `SandboxAPI.from_env(prefix="SANDBOX_")` reads
64
66
  `SANDBOX_SERVICE_URL`, `SANDBOX_SERVICE_TOKEN`, and optional
@@ -73,6 +75,7 @@ Public exports live in `drukbox_sdk`:
73
75
  - `SandboxTemplate`
74
76
  - `Secret`
75
77
  - `Issuer`
78
+ - `ServiceAccount`
76
79
  - `DoctorReport` and `DoctorCheck`
77
80
  - `HTTPProxy` and `HTTPProxyAttachment`
78
81
  - `SandboxAPIError` and typed subclasses for auth, not found, conflict,
@@ -86,6 +89,7 @@ Supported host operations:
86
89
  - `list_hosts`
87
90
  - `renew_host`
88
91
  - `delete_host`
92
+ - `refresh_secret`
89
93
  - `doctor`
90
94
  - `aclose`
91
95
 
@@ -127,6 +131,24 @@ The response carries no secret. A service name Drukbox does not know raises
127
131
  `expires_at` to extend by the service's default TTL; renewal never makes a host
128
132
  permanent.
129
133
 
134
+ `SandboxHost.service_account` names the service account that created or
135
+ claimed the host, `admin` for an admin key, or `None` for an unclaimed warm
136
+ host.
137
+
138
+ `refresh_secret` makes the service forget the current value of one secret on
139
+ a host and fetch a new one from its issuer. A static secret raises
140
+ `SandboxConflictError`; an issuer or exchange that did not answer raises
141
+ `SandboxUnavailableError`.
142
+
143
+ Supported service account operations, admin key only:
144
+
145
+ - `create_service_account`
146
+ - `remove_service_account`
147
+
148
+ `create_service_account` returns a `ServiceAccount` with the token the
149
+ service hands out once. Store it. `remove_service_account` revokes it on the
150
+ next request.
151
+
130
152
  Supported template operations:
131
153
 
132
154
  - `create_template`
@@ -153,7 +175,8 @@ proxy at a host's backing VM (the host must be `bootstrapping` or `active`).
153
175
 
154
176
  `doctor` fetches `GET /doctor` — read-only dependency health. The service
155
177
  runs one cheap, non-mutating probe per dependency (database, active VM
156
- provider, Tailscale when enabled) and always responds 200, so callers branch
178
+ provider, secrets exchange, Tailscale when enabled) and always responds 200,
179
+ so callers branch
157
180
  on `DoctorReport.ok` rather than the HTTP status. A failed `DoctorCheck`
158
181
  carries a stable `hint` slug for remediation.
159
182
 
@@ -0,0 +1,7 @@
1
+ drukbox_sdk/__init__.py,sha256=OgssAANGnl49_1N_EKMXSDlgWfedO9jUAnLVxgkJY2Q,1066
2
+ drukbox_sdk/api.py,sha256=Xd_UhCYsE_kRojDEBIacHGHrOB2msDHfrE1WfOBbOBc,24501
3
+ drukbox_sdk/exceptions.py,sha256=0CKbb0Ec4RSWPB8t31WJgO1yQQ10BwTPqoqS1_EieB4,2599
4
+ drukbox_python_sdk-0.4.0.dist-info/METADATA,sha256=kFilTF-v_ongltIWRUJrc1TLboG29gWCLCwABTPTmGo,6734
5
+ drukbox_python_sdk-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ drukbox_python_sdk-0.4.0.dist-info/licenses/LICENSE,sha256=zM66rWL3BWGJBa_mZPnM0WUomSSZ1xtWVtGQovQ71M8,1078
7
+ drukbox_python_sdk-0.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
drukbox_sdk/__init__.py CHANGED
@@ -14,10 +14,12 @@ from .api import (
14
14
  SandboxHost,
15
15
  SandboxTemplate,
16
16
  Secret,
17
+ ServiceAccount,
17
18
  )
18
19
  from .exceptions import (
19
20
  SandboxAPIError,
20
21
  SandboxAuthError,
22
+ SandboxCapacityError,
21
23
  SandboxConflictError,
22
24
  SandboxNotFoundError,
23
25
  SandboxProvisioningError,
@@ -35,6 +37,7 @@ __all__ = [
35
37
  "SandboxAPI",
36
38
  "SandboxAPIError",
37
39
  "SandboxAuthError",
40
+ "SandboxCapacityError",
38
41
  "SandboxConflictError",
39
42
  "SandboxHost",
40
43
  "SandboxNotFoundError",
@@ -44,4 +47,5 @@ __all__ = [
44
47
  "SandboxUnavailableError",
45
48
  "SandboxValidationError",
46
49
  "Secret",
50
+ "ServiceAccount",
47
51
  ]
drukbox_sdk/api.py CHANGED
@@ -47,6 +47,7 @@ import httpx
47
47
 
48
48
  from .exceptions import (
49
49
  SandboxAuthError,
50
+ SandboxCapacityError,
50
51
  SandboxConflictError,
51
52
  SandboxNotFoundError,
52
53
  SandboxProvisioningError,
@@ -85,10 +86,15 @@ class SandboxHost:
85
86
  different SSH auth model (exe.dev's edge proxy, Tailscale ACLs
86
87
  via tailscaled-SSH) return None here. A later ``get_host`` call
87
88
  always returns None — the key is not persisted server-side.
89
+
90
+ ``service_account`` names the service account that created or claimed
91
+ the host, ``"admin"`` for an admin key, or None for an unclaimed warm
92
+ host.
88
93
  """
89
94
 
90
95
  id: str
91
96
  name: str
97
+ service_account: str | None
92
98
  status: str
93
99
  provider: str
94
100
  image: str
@@ -196,6 +202,15 @@ class DoctorReport:
196
202
  checks: list[DoctorCheck]
197
203
 
198
204
 
205
+ @dataclass(frozen=True)
206
+ class ServiceAccount:
207
+ """A name the service knows a caller by, and the token that caller
208
+ presents. The service returns the token once, at creation."""
209
+
210
+ name: str
211
+ token: str = field(repr=False)
212
+
213
+
199
214
  @dataclass(frozen=True)
200
215
  class HTTPProxy:
201
216
  """An account-bound HTTP proxy as returned by the service.
@@ -401,6 +416,18 @@ class SandboxAPI:
401
416
  async def delete_host(self, host_id: uuid.UUID | str) -> None:
402
417
  await self._request("DELETE", f"/hosts/{host_id}")
403
418
 
419
+ async def refresh_secret(self, host_id: uuid.UUID | str, service: str) -> None:
420
+ """Make the service forget the current value of ``service`` on the
421
+ host and fetch a new one from its issuer.
422
+
423
+ Raises :class:`SandboxNotFoundError` for an unknown host or secret,
424
+ :class:`SandboxConflictError` for a secret with a static value, and
425
+ :class:`SandboxUnavailableError` when the exchange or the issuer did
426
+ not answer.
427
+ """
428
+
429
+ await self._request("POST", f"/hosts/{host_id}/secrets/{service}/refresh")
430
+
404
431
  async def create_template(
405
432
  self,
406
433
  *,
@@ -409,14 +436,15 @@ class SandboxAPI:
409
436
  base_image: str | None = None,
410
437
  label: str | None = None,
411
438
  ) -> SandboxTemplate:
412
- """Start a template build and return immediately.
413
-
414
- The service responds 202 with ``status="building"``. Poll
415
- :meth:`get_template` until the status becomes ``"available"`` or
416
- ``"failed"``. A failed record carries the reason in
417
- ``last_error``. A repeated post with the same provider, base
418
- image, and setup script returns the existing record without a
419
- rebuild.
439
+ """Start a template build.
440
+
441
+ The service pulls the base image, then responds 202 with
442
+ ``status="building"``. Poll :meth:`get_template` until the status
443
+ becomes ``"available"`` or ``"failed"``. A failed record carries the
444
+ reason in ``last_error``. A repeated post with the same provider,
445
+ base image digest, and setup script returns the existing record. A
446
+ ``"failed"`` record builds again and keeps its ``last_error`` until
447
+ that build ends.
420
448
  """
421
449
 
422
450
  payload: dict[str, Any] = {"setup_script": setup_script}
@@ -466,6 +494,27 @@ class SandboxAPI:
466
494
  checks=[DoctorCheck(**check) for check in data["checks"]],
467
495
  )
468
496
 
497
+ async def create_service_account(self, name: str) -> ServiceAccount:
498
+ """Create a service account and return its token.
499
+
500
+ Only an admin key from the service's ``SERVICE_TOKENS`` may call
501
+ this. A name that already exists raises
502
+ :class:`SandboxConflictError`; an invalid name raises
503
+ :class:`SandboxValidationError`.
504
+ """
505
+
506
+ data = await self._request("POST", "/service-accounts", json={"name": name})
507
+ return ServiceAccount(**data)
508
+
509
+ async def remove_service_account(self, name: str) -> None:
510
+ """Revoke a service account's token and remove the account.
511
+
512
+ An unknown name raises :class:`SandboxNotFoundError`; the ``admin``
513
+ account raises :class:`SandboxConflictError`.
514
+ """
515
+
516
+ await self._request("DELETE", f"/service-accounts/{name}")
517
+
469
518
  async def create_http_proxy(
470
519
  self,
471
520
  *,
@@ -567,6 +616,11 @@ class SandboxAPI:
567
616
  raise SandboxProvisioningError(json_response.get("detail", "provisioning failed"))
568
617
 
569
618
  if response.status_code == 503:
619
+ if json_response.get("error_code") == "CAPACITY":
620
+ raise SandboxCapacityError(
621
+ json_response.get("detail", "provider at host limit"),
622
+ retry_after=int(response.headers["Retry-After"]),
623
+ )
570
624
  raise SandboxUnavailableError(json_response.get("detail", "service unavailable"))
571
625
 
572
626
  if response.status_code == 422:
drukbox_sdk/exceptions.py CHANGED
@@ -18,6 +18,18 @@ class SandboxUnavailableError(RuntimeError):
18
18
  """
19
19
 
20
20
 
21
+ class SandboxCapacityError(SandboxUnavailableError):
22
+ """503 — the provider is at its host limit.
23
+
24
+ ``retry_after`` is the service's ``Retry-After`` value in seconds:
25
+ wait at least that long before the next create.
26
+ """
27
+
28
+ def __init__(self, message: str, retry_after: int) -> None:
29
+ super().__init__(message)
30
+ self.retry_after = retry_after
31
+
32
+
21
33
  class SandboxAPIError(RuntimeError):
22
34
  """Base for any error the sandbox service returned via HTTP."""
23
35
 
@@ -1,7 +0,0 @@
1
- drukbox_sdk/__init__.py,sha256=7LHAef5BXUaEN_pk9pXeowyV8CuQxWnbghANSZZTcPM,970
2
- drukbox_sdk/api.py,sha256=tcVaHQom-O2Xq8BbThFTjiRt7zHkWw3a6WYwC4rW3c0,22300
3
- drukbox_sdk/exceptions.py,sha256=Vzs4zVOywWsWfiZm6vgQ65ScI3Nn7zLGVj-8eT1YAR0,2224
4
- drukbox_python_sdk-0.2.0.dist-info/METADATA,sha256=3XKJgIsi_BHqaCuNhNaOSa6MSje_67u40BlCATpm3s4,5870
5
- drukbox_python_sdk-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
- drukbox_python_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=zM66rWL3BWGJBa_mZPnM0WUomSSZ1xtWVtGQovQ71M8,1078
7
- drukbox_python_sdk-0.2.0.dist-info/RECORD,,