mona-preview 0.1.0__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.
- mona/__init__.py +67 -0
- mona/_client.py +365 -0
- mona/_errors.py +179 -0
- mona/_models.py +290 -0
- mona/_ops.py +151 -0
- mona/_transport.py +74 -0
- mona/_version.py +1 -0
- mona/database.py +528 -0
- mona/resources/__init__.py +5 -0
- mona/resources/databases.py +215 -0
- mona_preview-0.1.0.dist-info/METADATA +137 -0
- mona_preview-0.1.0.dist-info/RECORD +14 -0
- mona_preview-0.1.0.dist-info/WHEEL +4 -0
- mona_preview-0.1.0.dist-info/licenses/LICENSE +190 -0
mona/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Official Python SDK for MonaDB.
|
|
2
|
+
|
|
3
|
+
Sync and async clients for managing hosted databases and executing SQL.
|
|
4
|
+
Built on :mod:`httpx` and :mod:`pydantic`.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Quick start with the synchronous client::
|
|
8
|
+
|
|
9
|
+
from mona import Client
|
|
10
|
+
|
|
11
|
+
with Client(api_key="mk-...", base_url="https://mona.example.workers.dev") as mo:
|
|
12
|
+
mo.databases.create(name="beatles")
|
|
13
|
+
rows = mo.database("beatles").query("select * from beatles;").rows
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from ._client import AsyncClient, Client
|
|
18
|
+
from ._errors import (
|
|
19
|
+
APIError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
BadRequestError,
|
|
22
|
+
ConflictError,
|
|
23
|
+
MonaError,
|
|
24
|
+
NotFoundError,
|
|
25
|
+
)
|
|
26
|
+
from ._models import (
|
|
27
|
+
AsyncDatabasePage,
|
|
28
|
+
DatabasePage,
|
|
29
|
+
ErrorResponse,
|
|
30
|
+
FieldError,
|
|
31
|
+
HealthStatus,
|
|
32
|
+
ProblemDetail,
|
|
33
|
+
ResolveDatabaseInstanceResponse,
|
|
34
|
+
Result,
|
|
35
|
+
Row,
|
|
36
|
+
Statement,
|
|
37
|
+
)
|
|
38
|
+
from ._models import (
|
|
39
|
+
Database as DatabaseRecord,
|
|
40
|
+
)
|
|
41
|
+
from ._version import __version__
|
|
42
|
+
from .database import AsyncDatabase, Database
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"APIError",
|
|
46
|
+
"AsyncClient",
|
|
47
|
+
"AsyncDatabase",
|
|
48
|
+
"AsyncDatabasePage",
|
|
49
|
+
"AuthenticationError",
|
|
50
|
+
"BadRequestError",
|
|
51
|
+
"Client",
|
|
52
|
+
"ConflictError",
|
|
53
|
+
"Database",
|
|
54
|
+
"DatabasePage",
|
|
55
|
+
"DatabaseRecord",
|
|
56
|
+
"ErrorResponse",
|
|
57
|
+
"FieldError",
|
|
58
|
+
"HealthStatus",
|
|
59
|
+
"MonaError",
|
|
60
|
+
"NotFoundError",
|
|
61
|
+
"ProblemDetail",
|
|
62
|
+
"ResolveDatabaseInstanceResponse",
|
|
63
|
+
"Result",
|
|
64
|
+
"Row",
|
|
65
|
+
"Statement",
|
|
66
|
+
"__version__",
|
|
67
|
+
]
|
mona/_client.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from . import _ops
|
|
9
|
+
from ._transport import Config, default_headers, raise_for_status, resolve_config
|
|
10
|
+
from .database import AsyncDatabase, Database
|
|
11
|
+
from .resources import AsyncDatabasesResource, DatabasesResource
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from types import TracebackType
|
|
15
|
+
|
|
16
|
+
from ._models import HealthStatus
|
|
17
|
+
from ._ops import Op
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Client:
|
|
21
|
+
"""Synchronous client for the Mona API.
|
|
22
|
+
|
|
23
|
+
Use as a context manager to ensure the underlying HTTP connection is closed,
|
|
24
|
+
or call :meth:`close` explicitly.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
databases: Control-plane and query helpers for hosted databases.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
Create a database and query it::
|
|
31
|
+
|
|
32
|
+
from mona import Client
|
|
33
|
+
|
|
34
|
+
with Client(api_key="mk-...", base_url="https://mona.example.workers.dev") as client:
|
|
35
|
+
client.databases.create(name="my-app")
|
|
36
|
+
rows = client.database("my-app").fetchall("select * from t;")
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str | None = None,
|
|
43
|
+
base_url: str | None = None,
|
|
44
|
+
*,
|
|
45
|
+
query_base_url: str | None = None,
|
|
46
|
+
default_database: str | None = None,
|
|
47
|
+
timeout: float = 30.0,
|
|
48
|
+
max_retries: int = 2,
|
|
49
|
+
http_client: httpx.Client | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Initialize a synchronous client.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
api_key: Bearer token for API authentication. Falls back to the
|
|
55
|
+
``MONA_API_KEY`` environment variable when omitted.
|
|
56
|
+
base_url: Control-plane base URL. Falls back to ``MONA_BASE_URL``.
|
|
57
|
+
query_base_url: Optional override for the data-plane host. Defaults
|
|
58
|
+
to ``base_url``.
|
|
59
|
+
default_database: Default database for :meth:`database`. Falls back
|
|
60
|
+
to ``MONA_DEFAULT_DATABASE``.
|
|
61
|
+
timeout: Per-request timeout in seconds.
|
|
62
|
+
max_retries: Connection-level retries passed to httpx.
|
|
63
|
+
http_client: Optional pre-configured :class:`httpx.Client`. When
|
|
64
|
+
provided, ``timeout`` and ``max_retries`` are not applied.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If no API key is available from arguments or the
|
|
68
|
+
environment.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
Configure from environment variables::
|
|
72
|
+
|
|
73
|
+
import os
|
|
74
|
+
|
|
75
|
+
os.environ["MONA_API_KEY"] = "mk-..."
|
|
76
|
+
os.environ["MONA_BASE_URL"] = "https://mona.example.workers.dev"
|
|
77
|
+
|
|
78
|
+
with Client() as client:
|
|
79
|
+
client.health()
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
self._config: Config = resolve_config(
|
|
83
|
+
api_key,
|
|
84
|
+
base_url,
|
|
85
|
+
query_base_url,
|
|
86
|
+
timeout,
|
|
87
|
+
max_retries,
|
|
88
|
+
default_database,
|
|
89
|
+
)
|
|
90
|
+
self._http = http_client or httpx.Client(
|
|
91
|
+
headers=default_headers(self._config.api_key),
|
|
92
|
+
timeout=self._config.timeout,
|
|
93
|
+
transport=httpx.HTTPTransport(retries=self._config.max_retries),
|
|
94
|
+
)
|
|
95
|
+
self.databases = DatabasesResource(self)
|
|
96
|
+
|
|
97
|
+
def database(self, name: str | None = None) -> Database:
|
|
98
|
+
"""Return a handle for running SQL against a database.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
name: Database name. Falls back to :attr:`default_database` when
|
|
102
|
+
omitted.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A :class:`~mona.Database` handle.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If no name is available from arguments or client config.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
Bind a database and query it::
|
|
112
|
+
|
|
113
|
+
db = client.database("my-app")
|
|
114
|
+
result = db.query("select {x: 1};")
|
|
115
|
+
|
|
116
|
+
Use the client default database::
|
|
117
|
+
|
|
118
|
+
client = Client(..., default_database="my-app")
|
|
119
|
+
rows = client.database().query("select * from my-app;").rows
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
resolved = name if name is not None else self._config.default_database
|
|
123
|
+
if not resolved:
|
|
124
|
+
msg = (
|
|
125
|
+
"database name is required: pass name=... or set default_database=... "
|
|
126
|
+
"or the MONA_DEFAULT_DATABASE environment variable"
|
|
127
|
+
)
|
|
128
|
+
raise ValueError(msg)
|
|
129
|
+
return Database(self, resolved)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def default_database(self) -> str | None:
|
|
133
|
+
"""Default database name for :meth:`database`."""
|
|
134
|
+
return self._config.default_database
|
|
135
|
+
|
|
136
|
+
def _url(self, op: Op) -> str:
|
|
137
|
+
base = self._config.query_base_url if op.plane == "query" else self._config.base_url
|
|
138
|
+
return f"{base}{op.path}"
|
|
139
|
+
|
|
140
|
+
def _send(self, op: Op) -> httpx.Response:
|
|
141
|
+
kwargs: dict[str, object] = {}
|
|
142
|
+
if op.timeout is not None:
|
|
143
|
+
kwargs["timeout"] = op.timeout
|
|
144
|
+
response = self._http.request(op.method, self._url(op), json=op.json, **kwargs)
|
|
145
|
+
return raise_for_status(response)
|
|
146
|
+
|
|
147
|
+
def health(self) -> HealthStatus:
|
|
148
|
+
"""Check API availability.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Parsed health payload from ``GET /health``.
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
Verify the service is up::
|
|
155
|
+
|
|
156
|
+
status = client.health()
|
|
157
|
+
assert status.status == "ok"
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
ep = _ops.health()
|
|
161
|
+
return ep.parse(self._send(ep.op))
|
|
162
|
+
|
|
163
|
+
def close(self) -> None:
|
|
164
|
+
"""Close the underlying HTTP client and release connections.
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
Explicit cleanup without a context manager::
|
|
168
|
+
|
|
169
|
+
client = Client(api_key="mk-...", base_url="https://api.example")
|
|
170
|
+
try:
|
|
171
|
+
client.databases.list()
|
|
172
|
+
finally:
|
|
173
|
+
client.close()
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
self._http.close()
|
|
177
|
+
|
|
178
|
+
def __enter__(self) -> Self:
|
|
179
|
+
"""Enter a context manager and return this client.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
This :class:`Client` instance.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def __exit__(
|
|
188
|
+
self,
|
|
189
|
+
exc_type: type[BaseException] | None,
|
|
190
|
+
exc: BaseException | None,
|
|
191
|
+
tb: TracebackType | None,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Exit the context manager and close the HTTP client.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
exc_type: Exception type, if an error was raised in the block.
|
|
197
|
+
exc: Exception instance, if raised.
|
|
198
|
+
tb: Traceback for the exception, if raised.
|
|
199
|
+
|
|
200
|
+
"""
|
|
201
|
+
self.close()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class AsyncClient:
|
|
205
|
+
"""Asynchronous client for the Mona API.
|
|
206
|
+
|
|
207
|
+
Use as an async context manager to ensure the underlying HTTP connection is
|
|
208
|
+
closed, or call :meth:`aclose` explicitly.
|
|
209
|
+
|
|
210
|
+
Attributes:
|
|
211
|
+
databases: Async control-plane and query helpers for hosted databases.
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
Create a database and query it::
|
|
215
|
+
|
|
216
|
+
from mona import AsyncClient
|
|
217
|
+
|
|
218
|
+
async with AsyncClient(
|
|
219
|
+
api_key="mk-...",
|
|
220
|
+
base_url="https://mona.example.workers.dev",
|
|
221
|
+
) as client:
|
|
222
|
+
await client.databases.create(name="my-app")
|
|
223
|
+
rows = await client.database("my-app").fetchall("select * from t;")
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
api_key: str | None = None,
|
|
230
|
+
base_url: str | None = None,
|
|
231
|
+
*,
|
|
232
|
+
query_base_url: str | None = None,
|
|
233
|
+
default_database: str | None = None,
|
|
234
|
+
timeout: float = 30.0,
|
|
235
|
+
max_retries: int = 2,
|
|
236
|
+
http_client: httpx.AsyncClient | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Initialize an asynchronous client.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
api_key: Bearer token for API authentication. Falls back to the
|
|
242
|
+
``MONA_API_KEY`` environment variable when omitted.
|
|
243
|
+
base_url: Control-plane base URL. Falls back to ``MONA_BASE_URL``.
|
|
244
|
+
query_base_url: Optional override for the data-plane host. Defaults
|
|
245
|
+
to ``base_url``.
|
|
246
|
+
default_database: Default database for :meth:`database`. Falls back
|
|
247
|
+
to ``MONA_DEFAULT_DATABASE``.
|
|
248
|
+
timeout: Per-request timeout in seconds.
|
|
249
|
+
max_retries: Connection-level retries passed to httpx.
|
|
250
|
+
http_client: Optional pre-configured :class:`httpx.AsyncClient`.
|
|
251
|
+
When provided, ``timeout`` and ``max_retries`` are not applied.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If no API key is available from arguments or the
|
|
255
|
+
environment.
|
|
256
|
+
|
|
257
|
+
Examples:
|
|
258
|
+
Configure from environment variables::
|
|
259
|
+
|
|
260
|
+
import os
|
|
261
|
+
|
|
262
|
+
os.environ["MONA_API_KEY"] = "mk-..."
|
|
263
|
+
os.environ["MONA_BASE_URL"] = "https://mona.example.workers.dev"
|
|
264
|
+
|
|
265
|
+
async with AsyncClient() as client:
|
|
266
|
+
await client.health()
|
|
267
|
+
|
|
268
|
+
"""
|
|
269
|
+
self._config: Config = resolve_config(
|
|
270
|
+
api_key,
|
|
271
|
+
base_url,
|
|
272
|
+
query_base_url,
|
|
273
|
+
timeout,
|
|
274
|
+
max_retries,
|
|
275
|
+
default_database,
|
|
276
|
+
)
|
|
277
|
+
self._http = http_client or httpx.AsyncClient(
|
|
278
|
+
headers=default_headers(self._config.api_key),
|
|
279
|
+
timeout=self._config.timeout,
|
|
280
|
+
transport=httpx.AsyncHTTPTransport(retries=self._config.max_retries),
|
|
281
|
+
)
|
|
282
|
+
self.databases = AsyncDatabasesResource(self)
|
|
283
|
+
|
|
284
|
+
def database(self, name: str | None = None) -> AsyncDatabase:
|
|
285
|
+
"""Return an async handle for running SQL against a database."""
|
|
286
|
+
resolved = name if name is not None else self._config.default_database
|
|
287
|
+
if not resolved:
|
|
288
|
+
msg = (
|
|
289
|
+
"database name is required: pass name=... or set default_database=... "
|
|
290
|
+
"or the MONA_DEFAULT_DATABASE environment variable"
|
|
291
|
+
)
|
|
292
|
+
raise ValueError(msg)
|
|
293
|
+
return AsyncDatabase(self, resolved)
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def default_database(self) -> str | None:
|
|
297
|
+
"""Default database name for :meth:`database`."""
|
|
298
|
+
return self._config.default_database
|
|
299
|
+
|
|
300
|
+
def _url(self, op: Op) -> str:
|
|
301
|
+
base = self._config.query_base_url if op.plane == "query" else self._config.base_url
|
|
302
|
+
return f"{base}{op.path}"
|
|
303
|
+
|
|
304
|
+
async def _send(self, op: Op) -> httpx.Response:
|
|
305
|
+
kwargs: dict[str, object] = {}
|
|
306
|
+
if op.timeout is not None:
|
|
307
|
+
kwargs["timeout"] = op.timeout
|
|
308
|
+
response = await self._http.request(op.method, self._url(op), json=op.json, **kwargs)
|
|
309
|
+
return raise_for_status(response)
|
|
310
|
+
|
|
311
|
+
async def health(self) -> HealthStatus:
|
|
312
|
+
"""Check API availability.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Parsed health payload from ``GET /health``.
|
|
316
|
+
|
|
317
|
+
Examples:
|
|
318
|
+
Verify the service is up::
|
|
319
|
+
|
|
320
|
+
status = await client.health()
|
|
321
|
+
assert status.status == "ok"
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
ep = _ops.health()
|
|
325
|
+
return ep.parse(await self._send(ep.op))
|
|
326
|
+
|
|
327
|
+
async def aclose(self) -> None:
|
|
328
|
+
"""Close the underlying HTTP client and release connections.
|
|
329
|
+
|
|
330
|
+
Examples:
|
|
331
|
+
Explicit cleanup without a context manager::
|
|
332
|
+
|
|
333
|
+
client = AsyncClient(api_key="mk-...", base_url="https://api.example")
|
|
334
|
+
try:
|
|
335
|
+
await client.databases.list()
|
|
336
|
+
finally:
|
|
337
|
+
await client.aclose()
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
await self._http.aclose()
|
|
341
|
+
|
|
342
|
+
async def __aenter__(self) -> Self:
|
|
343
|
+
"""Enter an async context manager and return this client.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
This :class:`AsyncClient` instance.
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
return self
|
|
350
|
+
|
|
351
|
+
async def __aexit__(
|
|
352
|
+
self,
|
|
353
|
+
exc_type: type[BaseException] | None,
|
|
354
|
+
exc: BaseException | None,
|
|
355
|
+
tb: TracebackType | None,
|
|
356
|
+
) -> None:
|
|
357
|
+
"""Exit the async context manager and close the HTTP client.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
exc_type: Exception type, if an error was raised in the block.
|
|
361
|
+
exc: Exception instance, if raised.
|
|
362
|
+
tb: Traceback for the exception, if raised.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
await self.aclose()
|
mona/_errors.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MonaError(Exception):
|
|
10
|
+
"""Base class for all errors raised by the Mona SDK.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
Catch any SDK error::
|
|
14
|
+
|
|
15
|
+
from mona import Client, MonaError
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
client.databases.get("missing")
|
|
19
|
+
except MonaError:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIError(MonaError):
|
|
26
|
+
"""An error response returned by the Mona API.
|
|
27
|
+
|
|
28
|
+
``code`` is the machine-readable error code from the control plane
|
|
29
|
+
(for example ``"not_found"``). For data-plane errors, which return a
|
|
30
|
+
plain-text body, it falls back to a code derived from the HTTP status.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
message: Human-readable error description.
|
|
34
|
+
status_code: HTTP status code from the response.
|
|
35
|
+
code: Machine-readable error code.
|
|
36
|
+
response: Original :class:`httpx.Response` that triggered the error.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
Inspect error details::
|
|
40
|
+
|
|
41
|
+
from mona import Client, NotFoundError
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
client.databases.get("missing")
|
|
45
|
+
except NotFoundError as exc:
|
|
46
|
+
print(exc.status_code, exc.code, exc.message)
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
message: str,
|
|
53
|
+
*,
|
|
54
|
+
status_code: int,
|
|
55
|
+
code: str,
|
|
56
|
+
response: httpx.Response,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize an API error.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
message: Human-readable error description.
|
|
62
|
+
status_code: HTTP status code from the response.
|
|
63
|
+
code: Machine-readable error code.
|
|
64
|
+
response: Original HTTP response.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
super().__init__(message)
|
|
68
|
+
self.message = message
|
|
69
|
+
self.status_code = status_code
|
|
70
|
+
self.code = code
|
|
71
|
+
self.response = response
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BadRequestError(APIError):
|
|
75
|
+
"""HTTP 400 — the request was malformed or invalid.
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
Handle validation failures::
|
|
79
|
+
|
|
80
|
+
from mona import BadRequestError
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
client.databases.create(name="")
|
|
84
|
+
except BadRequestError as exc:
|
|
85
|
+
print(exc.message)
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AuthenticationError(APIError):
|
|
91
|
+
"""HTTP 401 — the API key is missing or invalid.
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
Detect auth problems::
|
|
95
|
+
|
|
96
|
+
from mona import AuthenticationError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
client.databases.list()
|
|
100
|
+
except AuthenticationError:
|
|
101
|
+
print("Check MONA_API_KEY")
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class NotFoundError(APIError):
|
|
107
|
+
"""HTTP 404 — the requested resource does not exist.
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
Handle a missing database::
|
|
111
|
+
|
|
112
|
+
from mona import NotFoundError
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
client.databases.get("unknown")
|
|
116
|
+
except NotFoundError as exc:
|
|
117
|
+
assert exc.code == "not_found"
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ConflictError(APIError):
|
|
123
|
+
"""HTTP 409 — the request conflicts with the current state.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
Handle duplicate database creation::
|
|
127
|
+
|
|
128
|
+
from mona import ConflictError
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
client.databases.create(name="existing")
|
|
132
|
+
except ConflictError as exc:
|
|
133
|
+
print(exc.message)
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_STATUS_TO_CLASS: dict[int, type[APIError]] = {
|
|
139
|
+
400: BadRequestError,
|
|
140
|
+
401: AuthenticationError,
|
|
141
|
+
404: NotFoundError,
|
|
142
|
+
409: ConflictError,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def error_from_response(response: httpx.Response) -> APIError:
|
|
147
|
+
"""Build the appropriate :class:`APIError` from an HTTP error response.
|
|
148
|
+
|
|
149
|
+
Handles both the control plane's JSON ``{"code", "message"}`` body and the
|
|
150
|
+
data plane's plain-text error body.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
response: HTTP response with a non-success status code.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A typed :class:`APIError` subclass when the status code is recognized,
|
|
157
|
+
otherwise a base :class:`APIError`.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
status = response.status_code
|
|
161
|
+
code = f"http_{status}"
|
|
162
|
+
message = response.text
|
|
163
|
+
|
|
164
|
+
content_type = response.headers.get("content-type", "")
|
|
165
|
+
if "application/json" in content_type:
|
|
166
|
+
try:
|
|
167
|
+
body = response.json()
|
|
168
|
+
except ValueError:
|
|
169
|
+
body = None
|
|
170
|
+
if isinstance(body, dict):
|
|
171
|
+
if "code" in body and "message" in body:
|
|
172
|
+
code = str(body["code"])
|
|
173
|
+
message = str(body["message"])
|
|
174
|
+
elif "title" in body and "status" in body:
|
|
175
|
+
code = str(body.get("type", code))
|
|
176
|
+
message = str(body.get("detail") or body["title"])
|
|
177
|
+
|
|
178
|
+
cls = _STATUS_TO_CLASS.get(status, APIError)
|
|
179
|
+
return cls(message, status_code=status, code=code, response=response)
|