jsondb-cloud 1.0.4__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.
@@ -0,0 +1,65 @@
1
+ """jsondb-cloud — The official Python SDK for jsondb.cloud.
2
+
3
+ Quick start::
4
+
5
+ from jsondb_cloud import JsonDB
6
+
7
+ db = JsonDB(api_key="jdb_sk_live_xxxx")
8
+ users = db.collection("users")
9
+
10
+ user = users.create({"name": "Alice", "email": "alice@example.com"})
11
+ print(user["_id"])
12
+
13
+ Async usage::
14
+
15
+ from jsondb_cloud import AsyncJsonDB
16
+
17
+ async with AsyncJsonDB(api_key="jdb_sk_live_xxxx") as db:
18
+ users = db.collection("users")
19
+ user = await users.create({"name": "Alice"})
20
+ """
21
+
22
+ from .client import AsyncJsonDB, JsonDB
23
+ from .collection import AsyncCollection, Collection
24
+ from .errors import (
25
+ ConflictError,
26
+ DocumentTooLargeError,
27
+ ForbiddenError,
28
+ JsonDBError,
29
+ NotFoundError,
30
+ QuotaExceededError,
31
+ RateLimitError,
32
+ ServerError,
33
+ UnauthorizedError,
34
+ ValidationError,
35
+ create_error,
36
+ )
37
+ from .models import BulkResult, BulkResultSummary, ListResult, Meta
38
+
39
+ __all__ = [
40
+ # Clients
41
+ "JsonDB",
42
+ "AsyncJsonDB",
43
+ # Collections
44
+ "Collection",
45
+ "AsyncCollection",
46
+ # Response models
47
+ "ListResult",
48
+ "Meta",
49
+ "BulkResult",
50
+ "BulkResultSummary",
51
+ # Errors
52
+ "JsonDBError",
53
+ "NotFoundError",
54
+ "ConflictError",
55
+ "ValidationError",
56
+ "UnauthorizedError",
57
+ "ForbiddenError",
58
+ "QuotaExceededError",
59
+ "RateLimitError",
60
+ "DocumentTooLargeError",
61
+ "ServerError",
62
+ "create_error",
63
+ ]
64
+
65
+ __version__ = "1.0.0"
jsondb_cloud/_http.py ADDED
@@ -0,0 +1,244 @@
1
+ """Low-level HTTP client wrappers with auto-retry for the jsondb.cloud API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import math
7
+ import time
8
+ from typing import Any, Dict, Optional
9
+
10
+ import httpx
11
+
12
+ from .errors import JsonDBError, create_error
13
+
14
+
15
+ # HTTP status codes that should trigger an automatic retry.
16
+ _RETRYABLE_STATUSES = frozenset({429, 500, 502, 503, 504})
17
+
18
+
19
+ def _backoff_delay(attempt: int, base_delay: float, max_delay: float) -> float:
20
+ """Calculate exponential backoff delay with jitter cap.
21
+
22
+ Args:
23
+ attempt: Zero-based attempt index (0 = first retry).
24
+ base_delay: Base delay in seconds.
25
+ max_delay: Maximum delay in seconds.
26
+
27
+ Returns:
28
+ Delay in seconds before the next retry.
29
+ """
30
+ return min(base_delay * math.pow(2, attempt), max_delay)
31
+
32
+
33
+ class SyncHTTPClient:
34
+ """Synchronous HTTP client wrapper around ``httpx.Client``.
35
+
36
+ Handles Bearer token auth, automatic retries on 429/5xx with exponential
37
+ backoff, and translation of error responses into ``JsonDBError`` subclasses.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ *,
43
+ api_key: str,
44
+ base_url: str = "https://api.jsondb.cloud",
45
+ max_retries: int = 3,
46
+ retry_base_delay: float = 1.0,
47
+ retry_max_delay: float = 10.0,
48
+ timeout: float = 30.0,
49
+ headers: Optional[Dict[str, str]] = None,
50
+ ) -> None:
51
+ self._api_key = api_key
52
+ self._base_url = base_url.rstrip("/")
53
+ self._max_retries = max_retries
54
+ self._retry_base_delay = retry_base_delay
55
+ self._retry_max_delay = retry_max_delay
56
+
57
+ default_headers = {
58
+ "Authorization": f"Bearer {api_key}",
59
+ "Content-Type": "application/json",
60
+ "Accept": "application/json",
61
+ }
62
+ if headers:
63
+ default_headers.update(headers)
64
+
65
+ self._client = httpx.Client(
66
+ base_url=self._base_url,
67
+ headers=default_headers,
68
+ timeout=httpx.Timeout(timeout),
69
+ )
70
+
71
+ def close(self) -> None:
72
+ """Close the underlying HTTP client."""
73
+ self._client.close()
74
+
75
+ def request(
76
+ self,
77
+ method: str,
78
+ path: str,
79
+ *,
80
+ json: Any = None,
81
+ headers: Optional[Dict[str, str]] = None,
82
+ ) -> Any:
83
+ """Execute an HTTP request with automatic retry.
84
+
85
+ Args:
86
+ method: HTTP method (``GET``, ``POST``, ``PUT``, ``PATCH``, ``DELETE``).
87
+ path: URL path relative to ``base_url`` (should start with ``/``).
88
+ json: JSON-serializable request body (for POST/PUT/PATCH).
89
+ headers: Extra headers to merge for this request only.
90
+
91
+ Returns:
92
+ Parsed JSON response body, or ``None`` for 204 responses.
93
+
94
+ Raises:
95
+ JsonDBError: On non-retryable API errors.
96
+ """
97
+ max_attempts = self._max_retries + 1
98
+ last_error: Optional[Exception] = None
99
+
100
+ for attempt in range(max_attempts):
101
+ try:
102
+ response = self._client.request(
103
+ method,
104
+ path,
105
+ json=json,
106
+ headers=headers,
107
+ )
108
+
109
+ # Retry on retryable status codes
110
+ if response.status_code in _RETRYABLE_STATUSES and attempt < max_attempts - 1:
111
+ delay = _backoff_delay(attempt, self._retry_base_delay, self._retry_max_delay)
112
+ time.sleep(delay)
113
+ continue
114
+
115
+ # No content
116
+ if response.status_code == 204:
117
+ return None
118
+
119
+ data = response.json()
120
+
121
+ if not response.is_success:
122
+ raise create_error(response.status_code, data)
123
+
124
+ return data
125
+
126
+ except JsonDBError:
127
+ raise
128
+ except Exception as exc:
129
+ last_error = exc
130
+ if attempt < max_attempts - 1:
131
+ delay = _backoff_delay(attempt, self._retry_base_delay, self._retry_max_delay)
132
+ time.sleep(delay)
133
+ continue
134
+
135
+ if last_error is not None:
136
+ raise last_error
137
+ raise JsonDBError("Request failed after retries") # pragma: no cover
138
+
139
+
140
+ class AsyncHTTPClient:
141
+ """Asynchronous HTTP client wrapper around ``httpx.AsyncClient``.
142
+
143
+ Handles Bearer token auth, automatic retries on 429/5xx with exponential
144
+ backoff, and translation of error responses into ``JsonDBError`` subclasses.
145
+ """
146
+
147
+ def __init__(
148
+ self,
149
+ *,
150
+ api_key: str,
151
+ base_url: str = "https://api.jsondb.cloud",
152
+ max_retries: int = 3,
153
+ retry_base_delay: float = 1.0,
154
+ retry_max_delay: float = 10.0,
155
+ timeout: float = 30.0,
156
+ headers: Optional[Dict[str, str]] = None,
157
+ ) -> None:
158
+ self._api_key = api_key
159
+ self._base_url = base_url.rstrip("/")
160
+ self._max_retries = max_retries
161
+ self._retry_base_delay = retry_base_delay
162
+ self._retry_max_delay = retry_max_delay
163
+
164
+ default_headers = {
165
+ "Authorization": f"Bearer {api_key}",
166
+ "Content-Type": "application/json",
167
+ "Accept": "application/json",
168
+ }
169
+ if headers:
170
+ default_headers.update(headers)
171
+
172
+ self._client = httpx.AsyncClient(
173
+ base_url=self._base_url,
174
+ headers=default_headers,
175
+ timeout=httpx.Timeout(timeout),
176
+ )
177
+
178
+ async def close(self) -> None:
179
+ """Close the underlying async HTTP client."""
180
+ await self._client.aclose()
181
+
182
+ async def request(
183
+ self,
184
+ method: str,
185
+ path: str,
186
+ *,
187
+ json: Any = None,
188
+ headers: Optional[Dict[str, str]] = None,
189
+ ) -> Any:
190
+ """Execute an async HTTP request with automatic retry.
191
+
192
+ Args:
193
+ method: HTTP method (``GET``, ``POST``, ``PUT``, ``PATCH``, ``DELETE``).
194
+ path: URL path relative to ``base_url`` (should start with ``/``).
195
+ json: JSON-serializable request body (for POST/PUT/PATCH).
196
+ headers: Extra headers to merge for this request only.
197
+
198
+ Returns:
199
+ Parsed JSON response body, or ``None`` for 204 responses.
200
+
201
+ Raises:
202
+ JsonDBError: On non-retryable API errors.
203
+ """
204
+ max_attempts = self._max_retries + 1
205
+ last_error: Optional[Exception] = None
206
+
207
+ for attempt in range(max_attempts):
208
+ try:
209
+ response = await self._client.request(
210
+ method,
211
+ path,
212
+ json=json,
213
+ headers=headers,
214
+ )
215
+
216
+ # Retry on retryable status codes
217
+ if response.status_code in _RETRYABLE_STATUSES and attempt < max_attempts - 1:
218
+ delay = _backoff_delay(attempt, self._retry_base_delay, self._retry_max_delay)
219
+ await asyncio.sleep(delay)
220
+ continue
221
+
222
+ # No content
223
+ if response.status_code == 204:
224
+ return None
225
+
226
+ data = response.json()
227
+
228
+ if not response.is_success:
229
+ raise create_error(response.status_code, data)
230
+
231
+ return data
232
+
233
+ except JsonDBError:
234
+ raise
235
+ except Exception as exc:
236
+ last_error = exc
237
+ if attempt < max_attempts - 1:
238
+ delay = _backoff_delay(attempt, self._retry_base_delay, self._retry_max_delay)
239
+ await asyncio.sleep(delay)
240
+ continue
241
+
242
+ if last_error is not None:
243
+ raise last_error
244
+ raise JsonDBError("Request failed after retries") # pragma: no cover
jsondb_cloud/client.py ADDED
@@ -0,0 +1,177 @@
1
+ """Client classes for the jsondb.cloud Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional, cast
6
+
7
+ from ._http import AsyncHTTPClient, SyncHTTPClient
8
+ from .collection import AsyncCollection, Collection
9
+
10
+
11
+ class JsonDB:
12
+ """Synchronous client for the jsondb.cloud API.
13
+
14
+ Usage::
15
+
16
+ from jsondb_cloud import JsonDB
17
+
18
+ db = JsonDB(api_key="jdb_sk_live_xxxx")
19
+ users = db.collection("users")
20
+
21
+ # Create a document
22
+ alice = users.create({"name": "Alice", "email": "alice@example.com"})
23
+
24
+ # Read it back
25
+ user = users.get(alice["_id"])
26
+
27
+ Supports use as a context manager::
28
+
29
+ with JsonDB(api_key="jdb_sk_live_xxxx") as db:
30
+ users = db.collection("users")
31
+ users.create({"name": "Alice"})
32
+
33
+ Args:
34
+ api_key: API key (``jdb_sk_live_*`` or ``jdb_sk_test_*``).
35
+ project: Project name. Defaults to ``"v1"``.
36
+ base_url: API base URL. Defaults to ``"https://api.jsondb.cloud"``.
37
+ max_retries: Maximum number of retries on 429/5xx. Defaults to ``3``.
38
+ retry_base_delay: Base delay in seconds for exponential backoff. Defaults to ``1.0``.
39
+ retry_max_delay: Maximum delay in seconds for exponential backoff. Defaults to ``10.0``.
40
+ timeout: Request timeout in seconds. Defaults to ``30.0``.
41
+ headers: Optional extra headers to include with every request.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ api_key: str,
47
+ *,
48
+ project: str = "v1",
49
+ base_url: str = "https://api.jsondb.cloud",
50
+ max_retries: int = 3,
51
+ retry_base_delay: float = 1.0,
52
+ retry_max_delay: float = 10.0,
53
+ timeout: float = 30.0,
54
+ headers: Optional[Dict[str, str]] = None,
55
+ ) -> None:
56
+ if not api_key:
57
+ raise ValueError("api_key is required")
58
+
59
+ self._project = project
60
+ self._http = SyncHTTPClient(
61
+ api_key=api_key,
62
+ base_url=base_url,
63
+ max_retries=max_retries,
64
+ retry_base_delay=retry_base_delay,
65
+ retry_max_delay=retry_max_delay,
66
+ timeout=timeout,
67
+ headers=headers,
68
+ )
69
+
70
+ def collection(self, name: str) -> Collection:
71
+ """Get a reference to a collection.
72
+
73
+ Args:
74
+ name: Collection name (e.g. ``"users"``, ``"posts"``).
75
+
76
+ Returns:
77
+ A :class:`Collection` instance bound to this client and project.
78
+ """
79
+ return Collection(name=name, project=self._project, http=self._http)
80
+
81
+ def list_collections(self) -> List[str]:
82
+ """List all collections in the project."""
83
+ data = self._http.request("GET", f"/{self._project}")
84
+ return cast(List[str], data.get("data", data) if isinstance(data, dict) else data)
85
+
86
+ def close(self) -> None:
87
+ """Close the underlying HTTP client and release resources."""
88
+ self._http.close()
89
+
90
+ def __enter__(self) -> "JsonDB":
91
+ return self
92
+
93
+ def __exit__(self, *args: Any) -> None:
94
+ self.close()
95
+
96
+
97
+ class AsyncJsonDB:
98
+ """Asynchronous client for the jsondb.cloud API.
99
+
100
+ Usage::
101
+
102
+ from jsondb_cloud import AsyncJsonDB
103
+
104
+ db = AsyncJsonDB(api_key="jdb_sk_live_xxxx")
105
+ users = db.collection("users")
106
+
107
+ alice = await users.create({"name": "Alice", "email": "alice@example.com"})
108
+ user = await users.get(alice["_id"])
109
+
110
+ Supports use as an async context manager::
111
+
112
+ async with AsyncJsonDB(api_key="jdb_sk_live_xxxx") as db:
113
+ users = db.collection("users")
114
+ await users.create({"name": "Alice"})
115
+
116
+ Args:
117
+ api_key: API key (``jdb_sk_live_*`` or ``jdb_sk_test_*``).
118
+ project: Project name. Defaults to ``"v1"``.
119
+ base_url: API base URL. Defaults to ``"https://api.jsondb.cloud"``.
120
+ max_retries: Maximum number of retries on 429/5xx. Defaults to ``3``.
121
+ retry_base_delay: Base delay in seconds for exponential backoff. Defaults to ``1.0``.
122
+ retry_max_delay: Maximum delay in seconds for exponential backoff. Defaults to ``10.0``.
123
+ timeout: Request timeout in seconds. Defaults to ``30.0``.
124
+ headers: Optional extra headers to include with every request.
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ api_key: str,
130
+ *,
131
+ project: str = "v1",
132
+ base_url: str = "https://api.jsondb.cloud",
133
+ max_retries: int = 3,
134
+ retry_base_delay: float = 1.0,
135
+ retry_max_delay: float = 10.0,
136
+ timeout: float = 30.0,
137
+ headers: Optional[Dict[str, str]] = None,
138
+ ) -> None:
139
+ if not api_key:
140
+ raise ValueError("api_key is required")
141
+
142
+ self._project = project
143
+ self._http = AsyncHTTPClient(
144
+ api_key=api_key,
145
+ base_url=base_url,
146
+ max_retries=max_retries,
147
+ retry_base_delay=retry_base_delay,
148
+ retry_max_delay=retry_max_delay,
149
+ timeout=timeout,
150
+ headers=headers,
151
+ )
152
+
153
+ def collection(self, name: str) -> AsyncCollection:
154
+ """Get a reference to a collection.
155
+
156
+ Args:
157
+ name: Collection name (e.g. ``"users"``, ``"posts"``).
158
+
159
+ Returns:
160
+ An :class:`AsyncCollection` instance bound to this client and project.
161
+ """
162
+ return AsyncCollection(name=name, project=self._project, http=self._http)
163
+
164
+ async def list_collections(self) -> List[str]:
165
+ """List all collections in the project."""
166
+ data = await self._http.request("GET", f"/{self._project}")
167
+ return cast(List[str], data.get("data", data) if isinstance(data, dict) else data)
168
+
169
+ async def close(self) -> None:
170
+ """Close the underlying async HTTP client and release resources."""
171
+ await self._http.close()
172
+
173
+ async def __aenter__(self) -> "AsyncJsonDB":
174
+ return self
175
+
176
+ async def __aexit__(self, *args: Any) -> None:
177
+ await self.close()