qbrix 0.1.3__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.
- qbrix/__init__.py +91 -0
- qbrix/_base_client.py +279 -0
- qbrix/_client.py +72 -0
- qbrix/_config.py +16 -0
- qbrix/_mod_client.py +22 -0
- qbrix/_proxies.py +33 -0
- qbrix/_util.py +25 -0
- qbrix/_version.py +1 -0
- qbrix/exception.py +90 -0
- qbrix/model/__init__.py +41 -0
- qbrix/model/agent.py +31 -0
- qbrix/model/auth.py +13 -0
- qbrix/model/common.py +27 -0
- qbrix/model/experiment.py +35 -0
- qbrix/model/gate.py +32 -0
- qbrix/model/pool.py +35 -0
- qbrix/py.typed +0 -0
- qbrix/resource/__init__.py +19 -0
- qbrix/resource/_base.py +105 -0
- qbrix/resource/agent.py +68 -0
- qbrix/resource/experiment.py +167 -0
- qbrix/resource/gate.py +184 -0
- qbrix/resource/pool.py +108 -0
- qbrix-0.1.3.dist-info/METADATA +195 -0
- qbrix-0.1.3.dist-info/RECORD +26 -0
- qbrix-0.1.3.dist-info/WHEEL +4 -0
qbrix/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from qbrix._client import AsyncQbrix
|
|
2
|
+
from qbrix._client import Qbrix
|
|
3
|
+
from qbrix._config import QbrixConfig
|
|
4
|
+
from qbrix._mod_client import _load_client
|
|
5
|
+
from qbrix._mod_client import _reset_client
|
|
6
|
+
from qbrix._proxies import AgentProxy
|
|
7
|
+
from qbrix._proxies import ExperimentProxy
|
|
8
|
+
from qbrix._proxies import GateProxy
|
|
9
|
+
from qbrix._proxies import PoolProxy
|
|
10
|
+
from qbrix._version import __version__
|
|
11
|
+
from qbrix.resource.agent import AgentResource
|
|
12
|
+
from qbrix.resource.experiment import ExperimentResource
|
|
13
|
+
from qbrix.resource.gate import GateResource
|
|
14
|
+
from qbrix.resource.pool import PoolResource
|
|
15
|
+
from qbrix.exception import AuthenticationError
|
|
16
|
+
from qbrix.exception import BadRequestError
|
|
17
|
+
from qbrix.exception import ConflictError
|
|
18
|
+
from qbrix.exception import ForbiddenError
|
|
19
|
+
from qbrix.exception import InternalServerError
|
|
20
|
+
from qbrix.exception import NotFoundError
|
|
21
|
+
from qbrix.exception import QbrixAPIError
|
|
22
|
+
from qbrix.exception import QbrixConnectionError
|
|
23
|
+
from qbrix.exception import QbrixError
|
|
24
|
+
from qbrix.exception import QbrixTimeoutError
|
|
25
|
+
from qbrix.exception import RateLimitedError
|
|
26
|
+
from qbrix.exception import ServiceUnavailableError
|
|
27
|
+
from qbrix.model import Arm
|
|
28
|
+
from qbrix.model import ArmCreate
|
|
29
|
+
from qbrix.model import Context
|
|
30
|
+
from qbrix.model import Experiment
|
|
31
|
+
from qbrix.model import ExperimentCreate
|
|
32
|
+
from qbrix.model import ExperimentUpdate
|
|
33
|
+
from qbrix.model import GateConfig
|
|
34
|
+
from qbrix.model import GateCreate
|
|
35
|
+
from qbrix.model import GateRule
|
|
36
|
+
from qbrix.model import PaginatedResponse
|
|
37
|
+
from qbrix.model import Pool
|
|
38
|
+
from qbrix.model import PoolCreate
|
|
39
|
+
from qbrix.model import PoolUpdate
|
|
40
|
+
from qbrix.model import SelectedArm
|
|
41
|
+
from qbrix.model import SelectResponse
|
|
42
|
+
|
|
43
|
+
# module-level resource proxies — no explicit client instantiation required.
|
|
44
|
+
# reads QBRIX_API_KEY and QBRIX_BASE_URL from environment on first use.
|
|
45
|
+
pool: PoolResource = PoolProxy().__as_proxied__() # type: ignore[assignment]
|
|
46
|
+
experiment: ExperimentResource = ExperimentProxy().__as_proxied__() # type: ignore[assignment]
|
|
47
|
+
gate: GateResource = GateProxy().__as_proxied__() # type: ignore[assignment]
|
|
48
|
+
agent: AgentResource = AgentProxy().__as_proxied__() # type: ignore[assignment]
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"__version__",
|
|
52
|
+
"AsyncQbrix",
|
|
53
|
+
"Qbrix",
|
|
54
|
+
"QbrixConfig",
|
|
55
|
+
# module-level proxies
|
|
56
|
+
"pool",
|
|
57
|
+
"experiment",
|
|
58
|
+
"gate",
|
|
59
|
+
"agent",
|
|
60
|
+
"_load_client",
|
|
61
|
+
"_reset_client",
|
|
62
|
+
# exceptions
|
|
63
|
+
"AuthenticationError",
|
|
64
|
+
"BadRequestError",
|
|
65
|
+
"ConflictError",
|
|
66
|
+
"ForbiddenError",
|
|
67
|
+
"InternalServerError",
|
|
68
|
+
"NotFoundError",
|
|
69
|
+
"QbrixAPIError",
|
|
70
|
+
"QbrixConnectionError",
|
|
71
|
+
"QbrixError",
|
|
72
|
+
"QbrixTimeoutError",
|
|
73
|
+
"RateLimitedError",
|
|
74
|
+
"ServiceUnavailableError",
|
|
75
|
+
# models
|
|
76
|
+
"Arm",
|
|
77
|
+
"ArmCreate",
|
|
78
|
+
"Context",
|
|
79
|
+
"Experiment",
|
|
80
|
+
"ExperimentCreate",
|
|
81
|
+
"ExperimentUpdate",
|
|
82
|
+
"GateConfig",
|
|
83
|
+
"GateCreate",
|
|
84
|
+
"GateRule",
|
|
85
|
+
"PaginatedResponse",
|
|
86
|
+
"Pool",
|
|
87
|
+
"PoolCreate",
|
|
88
|
+
"PoolUpdate",
|
|
89
|
+
"SelectedArm",
|
|
90
|
+
"SelectResponse",
|
|
91
|
+
]
|
qbrix/_base_client.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from qbrix._config import QbrixConfig
|
|
13
|
+
from qbrix.exception import QbrixAPIError
|
|
14
|
+
from qbrix.exception import QbrixConnectionError
|
|
15
|
+
from qbrix.exception import QbrixTimeoutError
|
|
16
|
+
from qbrix.exception import RateLimitedError
|
|
17
|
+
from qbrix.exception import STATUS_CODE_TO_EXCEPTION
|
|
18
|
+
|
|
19
|
+
_T = TypeVar("_T", bound=BaseModel)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseClient:
|
|
23
|
+
_config: QbrixConfig
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
api_key: str | None = None,
|
|
29
|
+
base_url: str | None = None,
|
|
30
|
+
timeout: float | None = None,
|
|
31
|
+
max_retries: int | None = None,
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
) -> None:
|
|
34
|
+
overrides: dict[str, Any] = {}
|
|
35
|
+
if api_key is not None:
|
|
36
|
+
overrides["api_key"] = api_key
|
|
37
|
+
if base_url is not None:
|
|
38
|
+
overrides["base_url"] = base_url
|
|
39
|
+
if timeout is not None:
|
|
40
|
+
overrides["timeout"] = timeout
|
|
41
|
+
if max_retries is not None:
|
|
42
|
+
overrides["max_retries"] = max_retries
|
|
43
|
+
overrides.update(kwargs)
|
|
44
|
+
self._config = QbrixConfig(**overrides)
|
|
45
|
+
|
|
46
|
+
def _build_headers(self) -> dict[str, str]:
|
|
47
|
+
headers: dict[str, str] = {"Accept": "application/json"}
|
|
48
|
+
if self._config.api_key:
|
|
49
|
+
headers["X-API-Key"] = self._config.api_key
|
|
50
|
+
return headers
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _make_status_error(response: httpx.Response) -> QbrixAPIError:
|
|
54
|
+
detail = ""
|
|
55
|
+
context = None
|
|
56
|
+
try:
|
|
57
|
+
body = response.json()
|
|
58
|
+
detail = body.get("detail", response.text)
|
|
59
|
+
context = body.get("context")
|
|
60
|
+
except Exception: # noqa
|
|
61
|
+
detail = response.text
|
|
62
|
+
|
|
63
|
+
status = response.status_code
|
|
64
|
+
exc_cls = STATUS_CODE_TO_EXCEPTION.get(status, QbrixAPIError)
|
|
65
|
+
|
|
66
|
+
if exc_cls is RateLimitedError:
|
|
67
|
+
retry_after_raw = response.headers.get("Retry-After")
|
|
68
|
+
retry_after = float(retry_after_raw) if retry_after_raw else None
|
|
69
|
+
return RateLimitedError(status, detail, context, retry_after)
|
|
70
|
+
|
|
71
|
+
return exc_cls(status, detail, context)
|
|
72
|
+
|
|
73
|
+
def _should_retry(self, response: httpx.Response) -> bool:
|
|
74
|
+
return response.status_code in self._config.retry_on
|
|
75
|
+
|
|
76
|
+
def _calculate_retry_delay(self, attempt: int) -> float:
|
|
77
|
+
delay = self._config.retry_base_delay * (2**attempt)
|
|
78
|
+
delay = min(delay, self._config.retry_max_delay)
|
|
79
|
+
return delay + random.uniform(0, delay * 0.1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SyncAPIClient(BaseClient):
|
|
83
|
+
_client: httpx.Client
|
|
84
|
+
|
|
85
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
86
|
+
super().__init__(**kwargs)
|
|
87
|
+
self._client = httpx.Client(
|
|
88
|
+
base_url=self._config.base_url,
|
|
89
|
+
headers=self._build_headers(),
|
|
90
|
+
timeout=self._config.timeout,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def request(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
path: str,
|
|
97
|
+
*,
|
|
98
|
+
body: dict[str, Any] | None = None,
|
|
99
|
+
params: dict[str, Any] | None = None,
|
|
100
|
+
cast_to: type[_T] | None = None,
|
|
101
|
+
) -> _T | dict[str, Any]:
|
|
102
|
+
last_exc: Exception | None = None
|
|
103
|
+
|
|
104
|
+
for attempt in range(self._config.max_retries + 1):
|
|
105
|
+
try:
|
|
106
|
+
response = self._client.request(
|
|
107
|
+
method, path, json=body, params=params
|
|
108
|
+
)
|
|
109
|
+
except httpx.ConnectError as exc:
|
|
110
|
+
raise QbrixConnectionError(str(exc)) from exc
|
|
111
|
+
except httpx.TimeoutException as exc:
|
|
112
|
+
raise QbrixTimeoutError(str(exc)) from exc
|
|
113
|
+
|
|
114
|
+
if response.is_success:
|
|
115
|
+
if response.status_code == 204 or not response.content:
|
|
116
|
+
return cast_to.model_validate({}) if cast_to else {}
|
|
117
|
+
data = response.json()
|
|
118
|
+
return cast_to.model_validate(data) if cast_to else data
|
|
119
|
+
|
|
120
|
+
if not self._should_retry(response):
|
|
121
|
+
raise self._make_status_error(response)
|
|
122
|
+
|
|
123
|
+
last_exc = self._make_status_error(response)
|
|
124
|
+
|
|
125
|
+
if attempt < self._config.max_retries:
|
|
126
|
+
time.sleep(self._calculate_retry_delay(attempt))
|
|
127
|
+
|
|
128
|
+
if last_exc:
|
|
129
|
+
raise last_exc
|
|
130
|
+
|
|
131
|
+
return {}
|
|
132
|
+
|
|
133
|
+
def get(
|
|
134
|
+
self,
|
|
135
|
+
path: str,
|
|
136
|
+
*,
|
|
137
|
+
cast_to: type[_T] | None = None,
|
|
138
|
+
params: dict[str, Any] | None = None,
|
|
139
|
+
) -> _T | dict[str, Any]:
|
|
140
|
+
return self.request("GET", path, cast_to=cast_to, params=params)
|
|
141
|
+
|
|
142
|
+
def post(
|
|
143
|
+
self,
|
|
144
|
+
path: str,
|
|
145
|
+
*,
|
|
146
|
+
body: dict[str, Any] | None = None,
|
|
147
|
+
cast_to: type[_T] | None = None,
|
|
148
|
+
) -> _T | dict[str, Any]:
|
|
149
|
+
return self.request("POST", path, body=body, cast_to=cast_to)
|
|
150
|
+
|
|
151
|
+
def put(
|
|
152
|
+
self,
|
|
153
|
+
path: str,
|
|
154
|
+
*,
|
|
155
|
+
body: dict[str, Any] | None = None,
|
|
156
|
+
cast_to: type[_T] | None = None,
|
|
157
|
+
) -> _T | dict[str, Any]:
|
|
158
|
+
return self.request("PUT", path, body=body, cast_to=cast_to)
|
|
159
|
+
|
|
160
|
+
def patch(
|
|
161
|
+
self,
|
|
162
|
+
path: str,
|
|
163
|
+
*,
|
|
164
|
+
body: dict[str, Any] | None = None,
|
|
165
|
+
cast_to: type[_T] | None = None,
|
|
166
|
+
) -> _T | dict[str, Any]:
|
|
167
|
+
return self.request("PATCH", path, body=body, cast_to=cast_to)
|
|
168
|
+
|
|
169
|
+
def delete(self, path: str) -> None:
|
|
170
|
+
self.request("DELETE", path)
|
|
171
|
+
|
|
172
|
+
def close(self) -> None:
|
|
173
|
+
self._client.close()
|
|
174
|
+
|
|
175
|
+
def __enter__(self) -> SyncAPIClient:
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
def __exit__(self, *args: Any) -> None:
|
|
179
|
+
self.close()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class AsyncAPIClient(BaseClient):
|
|
183
|
+
_client: httpx.AsyncClient
|
|
184
|
+
|
|
185
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
186
|
+
super().__init__(**kwargs)
|
|
187
|
+
self._client = httpx.AsyncClient(
|
|
188
|
+
base_url=self._config.base_url,
|
|
189
|
+
headers=self._build_headers(),
|
|
190
|
+
timeout=self._config.timeout,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def request(
|
|
194
|
+
self,
|
|
195
|
+
method: str,
|
|
196
|
+
path: str,
|
|
197
|
+
*,
|
|
198
|
+
body: dict[str, Any] | None = None,
|
|
199
|
+
params: dict[str, Any] | None = None,
|
|
200
|
+
cast_to: type[_T] | None = None,
|
|
201
|
+
) -> _T | dict[str, Any]:
|
|
202
|
+
last_exc: Exception | None = None
|
|
203
|
+
|
|
204
|
+
for attempt in range(self._config.max_retries + 1):
|
|
205
|
+
try:
|
|
206
|
+
response = await self._client.request(
|
|
207
|
+
method, path, json=body, params=params
|
|
208
|
+
)
|
|
209
|
+
except httpx.ConnectError as exc:
|
|
210
|
+
raise QbrixConnectionError(str(exc)) from exc
|
|
211
|
+
except httpx.TimeoutException as exc:
|
|
212
|
+
raise QbrixTimeoutError(str(exc)) from exc
|
|
213
|
+
|
|
214
|
+
if response.is_success:
|
|
215
|
+
if response.status_code == 204 or not response.content:
|
|
216
|
+
return cast_to.model_validate({}) if cast_to else {}
|
|
217
|
+
data = response.json()
|
|
218
|
+
return cast_to.model_validate(data) if cast_to else data
|
|
219
|
+
|
|
220
|
+
if not self._should_retry(response):
|
|
221
|
+
raise self._make_status_error(response)
|
|
222
|
+
|
|
223
|
+
last_exc = self._make_status_error(response)
|
|
224
|
+
|
|
225
|
+
if attempt < self._config.max_retries:
|
|
226
|
+
await asyncio.sleep(self._calculate_retry_delay(attempt))
|
|
227
|
+
|
|
228
|
+
if last_exc:
|
|
229
|
+
raise last_exc
|
|
230
|
+
|
|
231
|
+
return {}
|
|
232
|
+
|
|
233
|
+
async def get(
|
|
234
|
+
self,
|
|
235
|
+
path: str,
|
|
236
|
+
*,
|
|
237
|
+
cast_to: type[_T] | None = None,
|
|
238
|
+
params: dict[str, Any] | None = None,
|
|
239
|
+
) -> _T | dict[str, Any]:
|
|
240
|
+
return await self.request("GET", path, cast_to=cast_to, params=params)
|
|
241
|
+
|
|
242
|
+
async def post(
|
|
243
|
+
self,
|
|
244
|
+
path: str,
|
|
245
|
+
*,
|
|
246
|
+
body: dict[str, Any] | None = None,
|
|
247
|
+
cast_to: type[_T] | None = None,
|
|
248
|
+
) -> _T | dict[str, Any]:
|
|
249
|
+
return await self.request("POST", path, body=body, cast_to=cast_to)
|
|
250
|
+
|
|
251
|
+
async def put(
|
|
252
|
+
self,
|
|
253
|
+
path: str,
|
|
254
|
+
*,
|
|
255
|
+
body: dict[str, Any] | None = None,
|
|
256
|
+
cast_to: type[_T] | None = None,
|
|
257
|
+
) -> _T | dict[str, Any]:
|
|
258
|
+
return await self.request("PUT", path, body=body, cast_to=cast_to)
|
|
259
|
+
|
|
260
|
+
async def patch(
|
|
261
|
+
self,
|
|
262
|
+
path: str,
|
|
263
|
+
*,
|
|
264
|
+
body: dict[str, Any] | None = None,
|
|
265
|
+
cast_to: type[_T] | None = None,
|
|
266
|
+
) -> _T | dict[str, Any]:
|
|
267
|
+
return await self.request("PATCH", path, body=body, cast_to=cast_to)
|
|
268
|
+
|
|
269
|
+
async def delete(self, path: str) -> None:
|
|
270
|
+
await self.request("DELETE", path)
|
|
271
|
+
|
|
272
|
+
async def close(self) -> None:
|
|
273
|
+
await self._client.aclose()
|
|
274
|
+
|
|
275
|
+
async def __aenter__(self) -> AsyncAPIClient:
|
|
276
|
+
return self
|
|
277
|
+
|
|
278
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
279
|
+
await self.close()
|
qbrix/_client.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
|
|
5
|
+
from qbrix._base_client import AsyncAPIClient
|
|
6
|
+
from qbrix._base_client import SyncAPIClient
|
|
7
|
+
from qbrix.resource.agent import AgentResource
|
|
8
|
+
from qbrix.resource.agent import AsyncAgentResource
|
|
9
|
+
from qbrix.resource.experiment import AsyncExperimentResource
|
|
10
|
+
from qbrix.resource.experiment import ExperimentResource
|
|
11
|
+
from qbrix.resource.gate import AsyncGateResource
|
|
12
|
+
from qbrix.resource.gate import GateResource
|
|
13
|
+
from qbrix.resource.pool import AsyncPoolResource
|
|
14
|
+
from qbrix.resource.pool import PoolResource
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Qbrix(SyncAPIClient):
|
|
18
|
+
"""synchronous qbrix SDK client.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
|
|
22
|
+
with Qbrix(api_key="optiq_xxx") as client:
|
|
23
|
+
result = client.agent.select("exp-id", context={"id": "user-1"})
|
|
24
|
+
client.agent.feedback(result.request_id, reward=1.0)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@cached_property
|
|
28
|
+
def pool(self) -> PoolResource:
|
|
29
|
+
return PoolResource(self)
|
|
30
|
+
|
|
31
|
+
@cached_property
|
|
32
|
+
def experiment(self) -> ExperimentResource:
|
|
33
|
+
return ExperimentResource(self)
|
|
34
|
+
|
|
35
|
+
@cached_property
|
|
36
|
+
def gate(self) -> GateResource:
|
|
37
|
+
return GateResource(self)
|
|
38
|
+
|
|
39
|
+
@cached_property
|
|
40
|
+
def agent(self) -> AgentResource:
|
|
41
|
+
return AgentResource(self)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AsyncQbrix(AsyncAPIClient):
|
|
45
|
+
"""asynchronous qbrix SDK client.
|
|
46
|
+
|
|
47
|
+
Usage::
|
|
48
|
+
|
|
49
|
+
async with AsyncQbrix(api_key="optiq_xxx") as client:
|
|
50
|
+
result = await client.agent.select("exp-id", context={"id": "user-1"})
|
|
51
|
+
await client.agent.feedback(result.request_id, reward=1.0)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@cached_property
|
|
55
|
+
def pool(self) -> AsyncPoolResource:
|
|
56
|
+
return AsyncPoolResource(self)
|
|
57
|
+
|
|
58
|
+
@cached_property
|
|
59
|
+
def experiment(self) -> AsyncExperimentResource:
|
|
60
|
+
return AsyncExperimentResource(self)
|
|
61
|
+
|
|
62
|
+
@cached_property
|
|
63
|
+
def gate(self) -> AsyncGateResource:
|
|
64
|
+
return AsyncGateResource(self)
|
|
65
|
+
|
|
66
|
+
@cached_property
|
|
67
|
+
def agent(self) -> AsyncAgentResource:
|
|
68
|
+
return AsyncAgentResource(self)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
Client = Qbrix
|
|
72
|
+
AsyncClient = AsyncQbrix
|
qbrix/_config.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
from pydantic_settings import SettingsConfigDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QbrixConfig(BaseSettings):
|
|
8
|
+
model_config = SettingsConfigDict(env_prefix="QBRIX_")
|
|
9
|
+
|
|
10
|
+
base_url: str = "http://localhost:8080"
|
|
11
|
+
api_key: str | None = None
|
|
12
|
+
timeout: float = 5.0
|
|
13
|
+
max_retries: int = 0
|
|
14
|
+
retry_on: tuple[int, ...] = (429, 502, 503, 504)
|
|
15
|
+
retry_base_delay: float = 0.5
|
|
16
|
+
retry_max_delay: float = 30.0
|
qbrix/_mod_client.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from qbrix._client import Qbrix
|
|
7
|
+
|
|
8
|
+
_mod_client: "Qbrix | None" = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _load_client() -> "Qbrix":
|
|
12
|
+
global _mod_client
|
|
13
|
+
if _mod_client is None:
|
|
14
|
+
from qbrix._client import Qbrix
|
|
15
|
+
_mod_client = Qbrix()
|
|
16
|
+
return _mod_client
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _reset_client() -> None:
|
|
20
|
+
"""Reset the module-level default client. Useful in tests."""
|
|
21
|
+
global _mod_client
|
|
22
|
+
_mod_client = None
|
qbrix/_proxies.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from qbrix._util import LazyProxy
|
|
4
|
+
from qbrix.resource.agent import AgentResource
|
|
5
|
+
from qbrix.resource.experiment import ExperimentResource
|
|
6
|
+
from qbrix.resource.gate import GateResource
|
|
7
|
+
from qbrix.resource.pool import PoolResource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_client() -> "Qbrix": # type: ignore[name-defined] # noqa: F821
|
|
11
|
+
from qbrix._mod_client import _load_client as _lc
|
|
12
|
+
|
|
13
|
+
return _lc()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PoolProxy(LazyProxy[PoolResource]):
|
|
17
|
+
def __load__(self) -> PoolResource:
|
|
18
|
+
return _load_client().pool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExperimentProxy(LazyProxy[ExperimentResource]):
|
|
22
|
+
def __load__(self) -> ExperimentResource:
|
|
23
|
+
return _load_client().experiment
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GateProxy(LazyProxy[GateResource]):
|
|
27
|
+
def __load__(self) -> GateResource:
|
|
28
|
+
return _load_client().gate
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentProxy(LazyProxy[AgentResource]):
|
|
32
|
+
def __load__(self) -> AgentResource:
|
|
33
|
+
return _load_client().agent
|
qbrix/_util.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LazyProxy(Generic[T]):
|
|
9
|
+
"""Defers instantiation of a resource until first attribute access."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
object.__setattr__(self, "_proxied", None)
|
|
13
|
+
|
|
14
|
+
def __load__(self) -> T:
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
def __getattr__(self, name: str) -> object:
|
|
18
|
+
proxied = object.__getattribute__(self, "_proxied")
|
|
19
|
+
if proxied is None:
|
|
20
|
+
proxied = self.__load__()
|
|
21
|
+
object.__setattr__(self, "_proxied", proxied)
|
|
22
|
+
return getattr(proxied, name)
|
|
23
|
+
|
|
24
|
+
def __as_proxied__(self) -> "LazyProxy[T]":
|
|
25
|
+
return self
|
qbrix/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3" # x-release-please-version
|
qbrix/exception.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class QbrixError(Exception):
|
|
7
|
+
"""base exception for all qbrix SDK errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QbrixAPIError(QbrixError):
|
|
11
|
+
"""error returned by the qbrix API."""
|
|
12
|
+
|
|
13
|
+
status_code: int
|
|
14
|
+
detail: str
|
|
15
|
+
context: dict[str, Any] | None
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
status_code: int,
|
|
20
|
+
detail: str,
|
|
21
|
+
context: dict[str, Any] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.detail = detail
|
|
25
|
+
self.context = context
|
|
26
|
+
super().__init__(f"[{status_code}] {detail}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BadRequestError(QbrixAPIError):
|
|
30
|
+
"""400 — malformed request."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthenticationError(QbrixAPIError):
|
|
34
|
+
"""401 — invalid or missing credentials."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ForbiddenError(QbrixAPIError):
|
|
38
|
+
"""403 — insufficient permissions."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NotFoundError(QbrixAPIError):
|
|
42
|
+
"""404 — resource not found."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConflictError(QbrixAPIError):
|
|
46
|
+
"""409 — resource conflict."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RateLimitedError(QbrixAPIError):
|
|
50
|
+
"""429 — rate limit exceeded."""
|
|
51
|
+
|
|
52
|
+
retry_after: float | None
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
status_code: int,
|
|
57
|
+
detail: str,
|
|
58
|
+
context: dict[str, Any] | None = None,
|
|
59
|
+
retry_after: float | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
super().__init__(status_code, detail, context)
|
|
62
|
+
self.retry_after = retry_after
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class InternalServerError(QbrixAPIError):
|
|
66
|
+
"""500 — server error."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ServiceUnavailableError(QbrixAPIError):
|
|
70
|
+
"""503 — downstream service failure."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class QbrixConnectionError(QbrixError):
|
|
74
|
+
"""failed to connect to the qbrix API."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class QbrixTimeoutError(QbrixError):
|
|
78
|
+
"""request to the qbrix API timed out."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
STATUS_CODE_TO_EXCEPTION: dict[int, type[QbrixAPIError]] = {
|
|
82
|
+
400: BadRequestError,
|
|
83
|
+
401: AuthenticationError,
|
|
84
|
+
403: ForbiddenError,
|
|
85
|
+
404: NotFoundError,
|
|
86
|
+
409: ConflictError,
|
|
87
|
+
429: RateLimitedError,
|
|
88
|
+
500: InternalServerError,
|
|
89
|
+
503: ServiceUnavailableError,
|
|
90
|
+
}
|
qbrix/model/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from qbrix.model.agent import FeedbackRequest
|
|
2
|
+
from qbrix.model.agent import FeedbackResponse
|
|
3
|
+
from qbrix.model.agent import SelectedArm
|
|
4
|
+
from qbrix.model.agent import SelectRequest
|
|
5
|
+
from qbrix.model.agent import SelectResponse
|
|
6
|
+
from qbrix.model.auth import APIKeyInfo
|
|
7
|
+
from qbrix.model.common import Context
|
|
8
|
+
from qbrix.model.common import PaginatedResponse
|
|
9
|
+
from qbrix.model.experiment import Experiment
|
|
10
|
+
from qbrix.model.experiment import ExperimentCreate
|
|
11
|
+
from qbrix.model.experiment import ExperimentUpdate
|
|
12
|
+
from qbrix.model.gate import GateConfig
|
|
13
|
+
from qbrix.model.gate import GateCreate
|
|
14
|
+
from qbrix.model.gate import GateRule
|
|
15
|
+
from qbrix.model.pool import Arm
|
|
16
|
+
from qbrix.model.pool import ArmCreate
|
|
17
|
+
from qbrix.model.pool import Pool
|
|
18
|
+
from qbrix.model.pool import PoolCreate
|
|
19
|
+
from qbrix.model.pool import PoolUpdate
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Arm",
|
|
23
|
+
"ArmCreate",
|
|
24
|
+
"APIKeyInfo",
|
|
25
|
+
"Context",
|
|
26
|
+
"Experiment",
|
|
27
|
+
"ExperimentCreate",
|
|
28
|
+
"ExperimentUpdate",
|
|
29
|
+
"FeedbackRequest",
|
|
30
|
+
"FeedbackResponse",
|
|
31
|
+
"GateConfig",
|
|
32
|
+
"GateCreate",
|
|
33
|
+
"GateRule",
|
|
34
|
+
"PaginatedResponse",
|
|
35
|
+
"Pool",
|
|
36
|
+
"PoolCreate",
|
|
37
|
+
"PoolUpdate",
|
|
38
|
+
"SelectedArm",
|
|
39
|
+
"SelectRequest",
|
|
40
|
+
"SelectResponse",
|
|
41
|
+
]
|