drukbox-python-sdk 0.4.0__tar.gz → 0.5.1__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.
@@ -57,6 +57,7 @@ run.
57
57
  consumers unless the user asks for them.
58
58
  - Preserve user changes in the worktree; do not clean or rewrite unrelated
59
59
  files.
60
- - A version bump rides alone: one commit (`chore: bump to X`) that touches
61
- only `pyproject.toml`, with no other change in it. Never fold the bump into
62
- a feature or fix commit. The release tag is cut from the bump commit.
60
+ - A version bump rides alone: its own commit (`chore: bump to X`) that
61
+ touches only `pyproject.toml`, in its own PR with no other change in it.
62
+ Never fold the bump into a feature or fix commit or PR. The release tag is
63
+ cut from the bump commit.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: drukbox-python-sdk
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "drukbox-python-sdk"
7
- version = "0.4.0"
7
+ version = "0.5.1"
8
8
  description = "Async Python client for Drukbox."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -198,7 +198,8 @@ class DoctorReport:
198
198
 
199
199
  ok: bool
200
200
  active_provider: str
201
- tailscale_enabled: bool
201
+ # Whether the active provider's hosts join the tailnet.
202
+ tailnet: bool
202
203
  checks: list[DoctorCheck]
203
204
 
204
205
 
@@ -490,7 +491,8 @@ class SandboxAPI:
490
491
  return DoctorReport(
491
492
  ok=data["ok"],
492
493
  active_provider=data["active_provider"],
493
- tailscale_enabled=data["tailscale_enabled"],
494
+ # drukbox before "Join the tailnet per provider" sent tailscale_enabled.
495
+ tailnet=data["tailnet"] if "tailnet" in data else data["tailscale_enabled"],
494
496
  checks=[DoctorCheck(**check) for check in data["checks"]],
495
497
  )
496
498
 
@@ -624,11 +626,11 @@ class SandboxAPI:
624
626
  raise SandboxUnavailableError(json_response.get("detail", "service unavailable"))
625
627
 
626
628
  if response.status_code == 422:
627
- # FastAPI's 422 detail is a list of error dicts; join the messages
628
- # so it reads as a line, not a raw list repr.
629
- errors = json_response.get("detail") or []
630
- detail = "; ".join(error.get("msg", "") for error in errors) or "validation error"
631
- raise SandboxValidationError(detail)
629
+ # The detail is FastAPI's list of error dicts or the service's own sentence.
630
+ detail = json_response.get("detail")
631
+ if isinstance(detail, list):
632
+ detail = "; ".join(error.get("msg", "") for error in detail)
633
+ raise SandboxValidationError(detail or "validation error")
632
634
 
633
635
  if response.status_code >= 400:
634
636
  raise SandboxResponseError(json_response.get("detail", "error"))
@@ -48,7 +48,7 @@ def _doctor_payload(**overrides: Any) -> dict[str, Any]:
48
48
  payload: dict[str, Any] = {
49
49
  "ok": True,
50
50
  "active_provider": "aws",
51
- "tailscale_enabled": True,
51
+ "tailnet": True,
52
52
  "checks": [
53
53
  {
54
54
  "name": "db",
@@ -744,6 +744,20 @@ async def test_422_raises_sandbox_validation_error(api: SandboxAPI):
744
744
  await api.create_host(env={"FOO BAR": "x"})
745
745
 
746
746
 
747
+ @respx.mock
748
+ async def test_422_string_detail_raises_sandbox_validation_error(api: SandboxAPI):
749
+ # Drukbox answers a refused secret with the issuer's reason as a plain string.
750
+ respx.post(f"{BASE_URL}/hosts").mock(
751
+ return_value=httpx.Response(
752
+ 422,
753
+ json={"detail": "GitHub App not installed for acme", "error_code": "SECRET_REFUSED"},
754
+ ),
755
+ )
756
+
757
+ with pytest.raises(SandboxValidationError, match=r"^GitHub App not installed for acme$"):
758
+ await api.create_host()
759
+
760
+
747
761
  @respx.mock
748
762
  async def test_500_raises_sandbox_response_error(api: SandboxAPI):
749
763
  respx.get(f"{BASE_URL}/hosts").mock(
@@ -925,6 +939,17 @@ def test_from_env_strips_trailing_slash_via_constructor(
925
939
  assert sandbox.base_url == "https://trailing.test"
926
940
 
927
941
 
942
+ @respx.mock
943
+ async def test_doctor_reads_tailscale_enabled_from_an_older_drukbox(api: SandboxAPI):
944
+ payload = _doctor_payload()
945
+ payload["tailscale_enabled"] = payload.pop("tailnet")
946
+ respx.get(f"{BASE_URL}/doctor").mock(return_value=httpx.Response(200, json=payload))
947
+
948
+ report = await api.doctor()
949
+
950
+ assert report.tailnet is True
951
+
952
+
928
953
  @respx.mock
929
954
  async def test_doctor_parses_report_and_checks(api: SandboxAPI):
930
955
  route = respx.get(f"{BASE_URL}/doctor").mock(
@@ -937,7 +962,7 @@ async def test_doctor_parses_report_and_checks(api: SandboxAPI):
937
962
  assert route.calls.last.request.headers["Authorization"] == "Bearer t-test"
938
963
  assert report.ok is True
939
964
  assert report.active_provider == "aws"
940
- assert report.tailscale_enabled is True
965
+ assert report.tailnet is True
941
966
  assert [check.name for check in report.checks] == ["db", "provider", "tailscale"]
942
967
  provider = next(check for check in report.checks if check.name == "provider")
943
968
  assert provider.status == "ok"