brokkr-client 0.3.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.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.3
2
+ Name: brokkr-client
3
+ Version: 0.3.0
4
+ Summary: Ergonomic Python wrapper around the auto-generated brokkr-client-generated low-level client
5
+ Requires-Dist: brokkr-client-generated
6
+ Requires-Dist: httpx>=0.23.0,<0.29.0
7
+ Requires-Dist: pytest>=8 ; extra == 'test'
8
+ Requires-Dist: pytest-asyncio>=0.23 ; extra == 'test'
9
+ Requires-Dist: respx>=0.21 ; extra == 'test'
10
+ Requires-Python: >=3.10
11
+ Provides-Extra: test
12
+ Description-Content-Type: text/markdown
13
+
14
+ # brokkr-client
15
+
16
+ Ergonomic Python client for the Brokkr broker API.
17
+
18
+ The import name remains `brokkr` (`from brokkr import BrokkrClient`); the
19
+ PyPI distribution name is `brokkr-client`.
20
+
21
+ This is a thin wrapper around the auto-generated `brokkr-client-generated`
22
+ package (produced by `openapi-python-client` from the broker's OpenAPI
23
+ spec). The wrapper adds:
24
+
25
+ - A single-credential constructor that injects the `Authorization` header
26
+ on every request. The three security schemes the spec declares
27
+ (`admin_pak` / `agent_pak` / `generator_pak`) all map to the same header
28
+ and the broker disambiguates at runtime — the wrapper hides that detail.
29
+ - `BrokkrError`, a single exception type that wraps the generated typed
30
+ `ErrorResponse` and exposes `.code` for stable pattern-matching.
31
+ - An opt-in `retry(...)` helper with exponential backoff for transient
32
+ transport / 5xx failures. Retry is per-call so callers decide which
33
+ operations (typically idempotent GETs) are safe.
34
+
35
+ Pagination iterators are intentionally absent: the v1 broker API returns
36
+ full collections without cursor tokens. `Stream`-style adapters belong
37
+ here when the API adds pagination.
38
+
39
+ The wrapper is intentionally small. Most of the surface is the generated
40
+ client; reach for it via `client.api` when the wrapper doesn't cover what
41
+ you need.
@@ -0,0 +1,28 @@
1
+ # brokkr-client
2
+
3
+ Ergonomic Python client for the Brokkr broker API.
4
+
5
+ The import name remains `brokkr` (`from brokkr import BrokkrClient`); the
6
+ PyPI distribution name is `brokkr-client`.
7
+
8
+ This is a thin wrapper around the auto-generated `brokkr-client-generated`
9
+ package (produced by `openapi-python-client` from the broker's OpenAPI
10
+ spec). The wrapper adds:
11
+
12
+ - A single-credential constructor that injects the `Authorization` header
13
+ on every request. The three security schemes the spec declares
14
+ (`admin_pak` / `agent_pak` / `generator_pak`) all map to the same header
15
+ and the broker disambiguates at runtime — the wrapper hides that detail.
16
+ - `BrokkrError`, a single exception type that wraps the generated typed
17
+ `ErrorResponse` and exposes `.code` for stable pattern-matching.
18
+ - An opt-in `retry(...)` helper with exponential backoff for transient
19
+ transport / 5xx failures. Retry is per-call so callers decide which
20
+ operations (typically idempotent GETs) are safe.
21
+
22
+ Pagination iterators are intentionally absent: the v1 broker API returns
23
+ full collections without cursor tokens. `Stream`-style adapters belong
24
+ here when the API adds pagination.
25
+
26
+ The wrapper is intentionally small. Most of the surface is the generated
27
+ client; reach for it via `client.api` when the wrapper doesn't cover what
28
+ you need.
@@ -0,0 +1,16 @@
1
+ """Ergonomic Python wrapper around brokkr-broker-client."""
2
+
3
+ from brokkr.client import BrokkrClient
4
+ from brokkr.errors import BrokkrError
5
+
6
+ # Re-export the typed ErrorResponse model so consumers don't need to dig into
7
+ # the generated package layout.
8
+ from brokkr_broker_client.models import ErrorResponse
9
+
10
+ # The generated `Generator` model clashes with `typing.Generator` and produces
11
+ # mypy false positives when imported into PEP 604 unions. Re-export under a
12
+ # clearer name; the original is still reachable as
13
+ # `brokkr_broker_client.models.Generator`.
14
+ from brokkr_broker_client.models import Generator as TemplateGenerator
15
+
16
+ __all__ = ["BrokkrClient", "BrokkrError", "ErrorResponse", "TemplateGenerator"]
@@ -0,0 +1,109 @@
1
+ """High-level Brokkr client wrapping the generated low-level client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any, Awaitable, Callable, TypeVar
7
+
8
+ import httpx
9
+
10
+ from brokkr_broker_client import AuthenticatedClient, Client
11
+ from brokkr_broker_client.models import ErrorResponse
12
+
13
+ from brokkr.errors import BrokkrError
14
+
15
+ T = TypeVar("T")
16
+
17
+ # Default timeouts mirror the Rust wrapper.
18
+ _DEFAULT_REQUEST_TIMEOUT = 30.0 # seconds
19
+ _DEFAULT_CONNECT_TIMEOUT = 10.0 # seconds
20
+ _DEFAULT_MAX_RETRIES = 3
21
+ _DEFAULT_INITIAL_BACKOFF = 0.2 # seconds
22
+ _MAX_BACKOFF = 10.0
23
+
24
+
25
+ class BrokkrClient:
26
+ """Ergonomic Brokkr broker client.
27
+
28
+ Construct with a base URL and (optionally) a PAK token. The wrapper
29
+ holds the generated `AuthenticatedClient` (or `Client`, when no token
30
+ is supplied) plus a retry policy. Access the generated API surface via
31
+ `client.api`; reach for the raw httpx session via `client.api.get_httpx_client()`.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ base_url: str,
37
+ *,
38
+ token: str | None = None,
39
+ request_timeout: float = _DEFAULT_REQUEST_TIMEOUT,
40
+ connect_timeout: float = _DEFAULT_CONNECT_TIMEOUT,
41
+ max_retries: int = _DEFAULT_MAX_RETRIES,
42
+ initial_backoff: float = _DEFAULT_INITIAL_BACKOFF,
43
+ ) -> None:
44
+ if max_retries < 0:
45
+ raise ValueError("max_retries must be >= 0")
46
+ if initial_backoff <= 0:
47
+ raise ValueError("initial_backoff must be > 0")
48
+
49
+ timeout = httpx.Timeout(request_timeout, connect=connect_timeout)
50
+ self._max_retries = max_retries
51
+ self._initial_backoff = initial_backoff
52
+
53
+ if token is not None:
54
+ self.api: AuthenticatedClient = AuthenticatedClient(
55
+ base_url=base_url,
56
+ token=token,
57
+ timeout=timeout,
58
+ )
59
+ else:
60
+ # mypy: AuthenticatedClient and Client share most of their surface
61
+ # but are technically distinct types. Users who construct without
62
+ # a token can only call endpoints that don't require auth.
63
+ self.api = Client(base_url=base_url, timeout=timeout) # type: ignore[assignment]
64
+
65
+ @property
66
+ def max_retries(self) -> int:
67
+ return self._max_retries
68
+
69
+ @property
70
+ def initial_backoff(self) -> float:
71
+ return self._initial_backoff
72
+
73
+ async def retry(self, op: Callable[[Any], Awaitable[T]]) -> T:
74
+ """Run ``op(client)`` with exponential backoff on retryable failures.
75
+
76
+ ``op`` is an async callable that takes the generated client
77
+ (``self.api``) and returns the operation's result. The closure form
78
+ keeps the wrapper free of per-endpoint glue while letting callers
79
+ decide which operations are safe to retry. Non-idempotent POSTs
80
+ should generally **not** be wrapped.
81
+
82
+ Retry classification matches the Rust wrapper: transport errors
83
+ and HTTP 408/429/502/503/504 qualify. Other 4xx/5xx responses
84
+ return immediately on the first failure.
85
+ """
86
+ attempt = 0
87
+ while True:
88
+ try:
89
+ result = await op(self.api)
90
+ except httpx.HTTPError as exc:
91
+ err = BrokkrError.from_transport(exc)
92
+ if not err.is_retryable() or attempt >= self._max_retries:
93
+ raise err from exc
94
+ else:
95
+ if isinstance(result, ErrorResponse):
96
+ # The generator folds documented errors into return
97
+ # unions; we surface them as raises here. We don't know
98
+ # the status code in this codepath — the *_detailed
99
+ # variants carry it. Callers wanting status-aware retry
100
+ # should use those and convert manually.
101
+ err = BrokkrError.from_response(result, status=500)
102
+ if not err.is_retryable() or attempt >= self._max_retries:
103
+ raise err
104
+ else:
105
+ return result
106
+
107
+ backoff = min(self._initial_backoff * (2**attempt), _MAX_BACKOFF)
108
+ await asyncio.sleep(backoff)
109
+ attempt += 1
@@ -0,0 +1,62 @@
1
+ """Typed error wrapper for the Brokkr SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from brokkr_broker_client.models import ErrorResponse
9
+
10
+ # HTTP status codes worth retrying — transient failures only. Mirrors the
11
+ # Rust wrapper's `is_retryable_status` set.
12
+ _RETRYABLE_STATUSES: frozenset[int] = frozenset({408, 429, 502, 503, 504})
13
+
14
+
15
+ @dataclass
16
+ class BrokkrError(Exception):
17
+ """Single exception type surfaced by the wrapper.
18
+
19
+ Carries the typed `ErrorResponse` body when the broker returned a
20
+ documented 4xx/5xx, and a status code when one is known. Callers
21
+ pattern-match on `code` (machine-readable, stable across versions)
22
+ rather than `message` (human-readable, not stable).
23
+ """
24
+
25
+ message: str
26
+ code: Optional[str] = None
27
+ status: Optional[int] = None
28
+ response: Optional[ErrorResponse] = None
29
+
30
+ def __post_init__(self) -> None:
31
+ super().__init__(self.message)
32
+
33
+ def __str__(self) -> str: # pragma: no cover - dataclass repr is fine
34
+ if self.status is not None and self.code is not None:
35
+ return f"{self.status} {self.code}: {self.message}"
36
+ if self.code is not None:
37
+ return f"{self.code}: {self.message}"
38
+ return self.message
39
+
40
+ def is_retryable(self) -> bool:
41
+ """Whether this error qualifies for the wrapper's retry helper."""
42
+ if self.status is None:
43
+ # Transport / unknown errors are retryable by default.
44
+ return True
45
+ return self.status in _RETRYABLE_STATUSES
46
+
47
+ @classmethod
48
+ def from_response(cls, body: ErrorResponse, status: int) -> "BrokkrError":
49
+ """Build from a generated `ErrorResponse` returned in an operation
50
+ union (rather than raised). The generator folds documented errors
51
+ into the return type union; the wrapper converts them to raises."""
52
+ return cls(
53
+ message=body.message,
54
+ code=body.code,
55
+ status=status,
56
+ response=body,
57
+ )
58
+
59
+ @classmethod
60
+ def from_transport(cls, exc: BaseException) -> "BrokkrError":
61
+ """Wrap an httpx / network exception."""
62
+ return cls(message=str(exc), code=None, status=None, response=None)
@@ -0,0 +1,34 @@
1
+ [project]
2
+ name = "brokkr-client"
3
+ version = "0.3.0"
4
+ description = "Ergonomic Python wrapper around the auto-generated brokkr-client-generated low-level client"
5
+ authors = []
6
+ requires-python = ">=3.10"
7
+ readme = "README.md"
8
+ dependencies = [
9
+ # Low-level generated client. Local path dep in-tree; resolved from PyPI
10
+ # for installed releases.
11
+ "brokkr-client-generated",
12
+ "httpx>=0.23.0,<0.29.0",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ test = [
17
+ "pytest>=8",
18
+ "pytest-asyncio>=0.23",
19
+ "respx>=0.21",
20
+ ]
21
+
22
+ [tool.uv.sources]
23
+ brokkr-client-generated = { path = "../brokkr-client" }
24
+
25
+ [tool.uv.build-backend]
26
+ module-name = "brokkr"
27
+ module-root = ""
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.11.0,<0.12.0"]
31
+ build-backend = "uv_build"
32
+
33
+ [tool.pytest.ini_options]
34
+ asyncio_mode = "auto"