drukbox-python-sdk 0.0.4__tar.gz → 0.0.6__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.
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/PKG-INFO +2 -2
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/README.md +1 -1
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/pyproject.toml +1 -1
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/src/drukbox_sdk/__init__.py +2 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/src/drukbox_sdk/api.py +16 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/src/drukbox_sdk/exceptions.py +9 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/tests/test_api.py +37 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/.github/workflows/on-main-merge.yml +0 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/.github/workflows/on-pull-request.yml +0 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/.github/workflows/release.yml +0 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/.gitignore +0 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/AGENTS.md +0 -0
- {drukbox_python_sdk-0.0.4 → drukbox_python_sdk-0.0.6}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drukbox-python-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Async Python client for Drukbox.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: httpx>=0.28
|
|
@@ -80,7 +80,7 @@ Supported host operations:
|
|
|
80
80
|
- `aclose`
|
|
81
81
|
|
|
82
82
|
`create_host` supports the service's optional `image`, `env`, `expires_at`,
|
|
83
|
-
and `Idempotency-Key` inputs.
|
|
83
|
+
`provider`, and `Idempotency-Key` inputs.
|
|
84
84
|
|
|
85
85
|
`doctor` fetches `GET /doctor` — read-only dependency health. The service
|
|
86
86
|
runs one cheap, non-mutating probe per dependency (database, active VM
|
|
@@ -72,7 +72,7 @@ Supported host operations:
|
|
|
72
72
|
- `aclose`
|
|
73
73
|
|
|
74
74
|
`create_host` supports the service's optional `image`, `env`, `expires_at`,
|
|
75
|
-
and `Idempotency-Key` inputs.
|
|
75
|
+
`provider`, and `Idempotency-Key` inputs.
|
|
76
76
|
|
|
77
77
|
`doctor` fetches `GET /doctor` — read-only dependency health. The service
|
|
78
78
|
runs one cheap, non-mutating probe per dependency (database, active VM
|
|
@@ -18,6 +18,7 @@ from .exceptions import (
|
|
|
18
18
|
SandboxProvisioningError,
|
|
19
19
|
SandboxResponseError,
|
|
20
20
|
SandboxUnavailableError,
|
|
21
|
+
SandboxValidationError,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
__all__ = [
|
|
@@ -32,4 +33,5 @@ __all__ = [
|
|
|
32
33
|
"SandboxProvisioningError",
|
|
33
34
|
"SandboxResponseError",
|
|
34
35
|
"SandboxUnavailableError",
|
|
36
|
+
"SandboxValidationError",
|
|
35
37
|
]
|
|
@@ -45,6 +45,7 @@ from .exceptions import (
|
|
|
45
45
|
SandboxProvisioningError,
|
|
46
46
|
SandboxResponseError,
|
|
47
47
|
SandboxUnavailableError,
|
|
48
|
+
SandboxValidationError,
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
# Explicit pool budget. The httpx defaults (100 / 20) are plenty for
|
|
@@ -214,6 +215,7 @@ class SandboxAPI:
|
|
|
214
215
|
expires_at: datetime | None = None,
|
|
215
216
|
idempotency_key: str | None = None,
|
|
216
217
|
image: str | None = None,
|
|
218
|
+
provider: str | None = None,
|
|
217
219
|
) -> SandboxHost:
|
|
218
220
|
"""Provision a new host.
|
|
219
221
|
|
|
@@ -228,6 +230,10 @@ class SandboxAPI:
|
|
|
228
230
|
header. Useful for retry safety on flaky networks — a retry with
|
|
229
231
|
the same key after a successful provision returns the original
|
|
230
232
|
host instead of creating a duplicate.
|
|
233
|
+
|
|
234
|
+
``provider`` pins which VM provider serves the request; omit it to
|
|
235
|
+
use the service default. An unknown provider raises
|
|
236
|
+
:class:`SandboxResponseError` (the service rejects it with 400).
|
|
231
237
|
"""
|
|
232
238
|
|
|
233
239
|
payload: dict[str, Any] = {}
|
|
@@ -237,6 +243,8 @@ class SandboxAPI:
|
|
|
237
243
|
payload["expires_at"] = expires_at.isoformat()
|
|
238
244
|
if image is not None:
|
|
239
245
|
payload["image"] = image
|
|
246
|
+
if provider is not None:
|
|
247
|
+
payload["provider"] = provider
|
|
240
248
|
|
|
241
249
|
headers: dict[str, str] = {}
|
|
242
250
|
if idempotency_key is not None:
|
|
@@ -343,6 +351,14 @@ class SandboxAPI:
|
|
|
343
351
|
if response.status_code == 503:
|
|
344
352
|
raise SandboxUnavailableError(json_response.get("detail", "service unavailable"))
|
|
345
353
|
|
|
354
|
+
if response.status_code == 422:
|
|
355
|
+
# FastAPI's 422 detail is a list of error dicts; join the messages
|
|
356
|
+
# so it reads as a line, not a raw list repr.
|
|
357
|
+
detail = json_response.get("detail", "validation error")
|
|
358
|
+
if isinstance(detail, list):
|
|
359
|
+
detail = "; ".join(item.get("msg", "") for item in detail) or "validation error"
|
|
360
|
+
raise SandboxValidationError(detail)
|
|
361
|
+
|
|
346
362
|
if response.status_code >= 400:
|
|
347
363
|
raise SandboxResponseError(json_response.get("detail", "error"))
|
|
348
364
|
return json_response
|
|
@@ -50,5 +50,14 @@ class SandboxProvisioningError(SandboxAPIError):
|
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
class SandboxValidationError(SandboxAPIError):
|
|
54
|
+
"""422 — the service rejected the request payload as invalid.
|
|
55
|
+
|
|
56
|
+
e.g. an env key that isn't a valid environment-variable name, or a
|
|
57
|
+
proxy target URL carrying a path or credentials. The request is
|
|
58
|
+
malformed, so retrying it unchanged won't help — fix the input.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
53
62
|
class SandboxResponseError(SandboxAPIError):
|
|
54
63
|
"""Everything else the service returned that the SDK doesn't classify."""
|
|
@@ -31,6 +31,7 @@ from drukbox_sdk import (
|
|
|
31
31
|
SandboxProvisioningError,
|
|
32
32
|
SandboxResponseError,
|
|
33
33
|
SandboxUnavailableError,
|
|
34
|
+
SandboxValidationError,
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
BASE_URL = "https://sandbox.test"
|
|
@@ -164,9 +165,23 @@ async def test_create_host_omits_image_and_env_when_unset(api: SandboxAPI):
|
|
|
164
165
|
assert "image" not in body
|
|
165
166
|
assert "env" not in body
|
|
166
167
|
assert "expires_at" not in body
|
|
168
|
+
assert "provider" not in body
|
|
167
169
|
assert "Idempotency-Key" not in route.calls.last.request.headers
|
|
168
170
|
|
|
169
171
|
|
|
172
|
+
@respx.mock
|
|
173
|
+
async def test_create_host_sends_provider_when_set(api: SandboxAPI):
|
|
174
|
+
payload = _host_payload(provider="hetzner")
|
|
175
|
+
route = respx.post(f"{BASE_URL}/hosts").mock(
|
|
176
|
+
return_value=httpx.Response(201, json=payload),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
host = await api.create_host(provider="hetzner")
|
|
180
|
+
|
|
181
|
+
assert '"provider":"hetzner"' in route.calls.last.request.content.decode()
|
|
182
|
+
assert host.provider == "hetzner"
|
|
183
|
+
|
|
184
|
+
|
|
170
185
|
@respx.mock
|
|
171
186
|
async def test_get_host_returns_parsed_host(api: SandboxAPI):
|
|
172
187
|
payload = _host_payload(status="active")
|
|
@@ -319,6 +334,28 @@ async def test_409_raises_sandbox_conflict_error(api: SandboxAPI):
|
|
|
319
334
|
await api.delete_host(host_id)
|
|
320
335
|
|
|
321
336
|
|
|
337
|
+
@respx.mock
|
|
338
|
+
async def test_422_raises_sandbox_validation_error(api: SandboxAPI):
|
|
339
|
+
# FastAPI's 422 detail is a list of error dicts; the SDK flattens it to a
|
|
340
|
+
# readable line and raises the dedicated validation error (don't-retry).
|
|
341
|
+
respx.post(f"{BASE_URL}/hosts").mock(
|
|
342
|
+
return_value=httpx.Response(
|
|
343
|
+
422,
|
|
344
|
+
json={
|
|
345
|
+
"detail": [
|
|
346
|
+
{
|
|
347
|
+
"loc": ["body", "env"],
|
|
348
|
+
"msg": "env keys must be valid environment variable names: FOO BAR",
|
|
349
|
+
}
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
with pytest.raises(SandboxValidationError, match="env keys must be valid"):
|
|
356
|
+
await api.create_host(env={"FOO BAR": "x"})
|
|
357
|
+
|
|
358
|
+
|
|
322
359
|
@respx.mock
|
|
323
360
|
async def test_500_raises_sandbox_response_error(api: SandboxAPI):
|
|
324
361
|
respx.get(f"{BASE_URL}/hosts").mock(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|