drukbox-python-sdk 0.3.0__tar.gz → 0.4.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.
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/PKG-INFO +3 -1
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/README.md +2 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/pyproject.toml +1 -1
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/src/drukbox_sdk/__init__.py +2 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/src/drukbox_sdk/api.py +15 -8
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/src/drukbox_sdk/exceptions.py +12 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/tests/test_api.py +22 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/.github/workflows/on-main-merge.yml +0 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/.github/workflows/on-pull-request.yml +0 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/.github/workflows/release.yml +0 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/.gitignore +0 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/AGENTS.md +0 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/LICENSE +0 -0
- {drukbox_python_sdk-0.3.0 → drukbox_python_sdk-0.4.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: drukbox-python-sdk
|
|
3
|
-
Version: 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
|
|
@@ -46,6 +46,8 @@ finally:
|
|
|
46
46
|
few minutes worst case. The SDK's default `timeout` (300s) covers this. Pass
|
|
47
47
|
an `idempotency_key` for retry safety: a retry with the same key after a
|
|
48
48
|
successful provision returns the original host instead of creating a duplicate.
|
|
49
|
+
A provider at its host limit raises `SandboxCapacityError`, a
|
|
50
|
+
`SandboxUnavailableError` whose `retry_after` gives the seconds to wait.
|
|
49
51
|
|
|
50
52
|
`SandboxAPI.from_env(prefix="SANDBOX_")` reads
|
|
51
53
|
`SANDBOX_SERVICE_URL`, `SANDBOX_SERVICE_TOKEN`, and optional
|
|
@@ -19,6 +19,7 @@ from .api import (
|
|
|
19
19
|
from .exceptions import (
|
|
20
20
|
SandboxAPIError,
|
|
21
21
|
SandboxAuthError,
|
|
22
|
+
SandboxCapacityError,
|
|
22
23
|
SandboxConflictError,
|
|
23
24
|
SandboxNotFoundError,
|
|
24
25
|
SandboxProvisioningError,
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"SandboxAPI",
|
|
37
38
|
"SandboxAPIError",
|
|
38
39
|
"SandboxAuthError",
|
|
40
|
+
"SandboxCapacityError",
|
|
39
41
|
"SandboxConflictError",
|
|
40
42
|
"SandboxHost",
|
|
41
43
|
"SandboxNotFoundError",
|
|
@@ -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,
|
|
@@ -435,14 +436,15 @@ class SandboxAPI:
|
|
|
435
436
|
base_image: str | None = None,
|
|
436
437
|
label: str | None = None,
|
|
437
438
|
) -> SandboxTemplate:
|
|
438
|
-
"""Start a template build
|
|
439
|
-
|
|
440
|
-
The service responds 202 with
|
|
441
|
-
:meth:`get_template` until the status
|
|
442
|
-
``"failed"``. A failed record carries the
|
|
443
|
-
``last_error``. A repeated post with the same provider,
|
|
444
|
-
image, and setup script returns the existing record
|
|
445
|
-
|
|
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.
|
|
446
448
|
"""
|
|
447
449
|
|
|
448
450
|
payload: dict[str, Any] = {"setup_script": setup_script}
|
|
@@ -614,6 +616,11 @@ class SandboxAPI:
|
|
|
614
616
|
raise SandboxProvisioningError(json_response.get("detail", "provisioning failed"))
|
|
615
617
|
|
|
616
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
|
+
)
|
|
617
624
|
raise SandboxUnavailableError(json_response.get("detail", "service unavailable"))
|
|
618
625
|
|
|
619
626
|
if response.status_code == 422:
|
|
@@ -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
|
|
|
@@ -28,6 +28,7 @@ from drukbox_sdk import (
|
|
|
28
28
|
Issuer,
|
|
29
29
|
SandboxAPI,
|
|
30
30
|
SandboxAuthError,
|
|
31
|
+
SandboxCapacityError,
|
|
31
32
|
SandboxConflictError,
|
|
32
33
|
SandboxNotFoundError,
|
|
33
34
|
SandboxProvisioningError,
|
|
@@ -796,6 +797,27 @@ async def test_503_raises_sandbox_unavailable_error(api: SandboxAPI):
|
|
|
796
797
|
await api.list_hosts()
|
|
797
798
|
|
|
798
799
|
|
|
800
|
+
@respx.mock
|
|
801
|
+
async def test_capacity_503_raises_sandbox_capacity_error(api: SandboxAPI):
|
|
802
|
+
"""A 503 with error_code CAPACITY is a host-limit refusal, not an
|
|
803
|
+
outage. It stays a SandboxUnavailableError so existing retries keep
|
|
804
|
+
working, and carries the service's Retry-After in seconds."""
|
|
805
|
+
|
|
806
|
+
respx.post(f"{BASE_URL}/hosts").mock(
|
|
807
|
+
return_value=httpx.Response(
|
|
808
|
+
503,
|
|
809
|
+
json={"detail": "provider 'exe' is at its host limit", "error_code": "CAPACITY"},
|
|
810
|
+
headers={"Retry-After": "30"},
|
|
811
|
+
),
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
with pytest.raises(SandboxCapacityError, match="host limit") as exc_info:
|
|
815
|
+
await api.create_host()
|
|
816
|
+
|
|
817
|
+
assert isinstance(exc_info.value, SandboxUnavailableError)
|
|
818
|
+
assert exc_info.value.retry_after == 30
|
|
819
|
+
|
|
820
|
+
|
|
799
821
|
@respx.mock
|
|
800
822
|
async def test_non_json_body_raises_sandbox_response_error(api: SandboxAPI):
|
|
801
823
|
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
|
|
File without changes
|