querri 1.0.2__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.
Files changed (64) hide show
  1. querri/__init__.py +51 -0
  2. querri/_auth.py +818 -0
  3. querri/_base_client.py +379 -0
  4. querri/_client.py +395 -0
  5. querri/_config.py +140 -0
  6. querri/_convenience.py +538 -0
  7. querri/_exceptions.py +180 -0
  8. querri/_pagination.py +278 -0
  9. querri/_streaming.py +663 -0
  10. querri/_user_client.py +193 -0
  11. querri/_version.py +1 -0
  12. querri/cli/__init__.py +21 -0
  13. querri/cli/_app.py +246 -0
  14. querri/cli/_context.py +216 -0
  15. querri/cli/_image.py +234 -0
  16. querri/cli/_output.py +293 -0
  17. querri/cli/audit.py +68 -0
  18. querri/cli/auth.py +556 -0
  19. querri/cli/chat.py +1255 -0
  20. querri/cli/chats.py +358 -0
  21. querri/cli/dashboards.py +294 -0
  22. querri/cli/embed.py +220 -0
  23. querri/cli/files.py +214 -0
  24. querri/cli/keys.py +304 -0
  25. querri/cli/policies.py +424 -0
  26. querri/cli/projects.py +916 -0
  27. querri/cli/sharing.py +403 -0
  28. querri/cli/sources.py +532 -0
  29. querri/cli/steps.py +123 -0
  30. querri/cli/usage.py +99 -0
  31. querri/cli/users.py +223 -0
  32. querri/cli/views.py +449 -0
  33. querri/cli/whoami.py +78 -0
  34. querri/py.typed +0 -0
  35. querri/resources/__init__.py +1 -0
  36. querri/resources/audit.py +119 -0
  37. querri/resources/dashboards.py +295 -0
  38. querri/resources/embed.py +268 -0
  39. querri/resources/files.py +145 -0
  40. querri/resources/keys.py +191 -0
  41. querri/resources/policies.py +492 -0
  42. querri/resources/projects.py +808 -0
  43. querri/resources/sharing.py +330 -0
  44. querri/resources/sources.py +511 -0
  45. querri/resources/usage.py +106 -0
  46. querri/resources/users.py +303 -0
  47. querri/resources/views.py +347 -0
  48. querri/types/__init__.py +104 -0
  49. querri/types/audit.py +21 -0
  50. querri/types/chat.py +51 -0
  51. querri/types/dashboard.py +51 -0
  52. querri/types/data.py +61 -0
  53. querri/types/embed.py +38 -0
  54. querri/types/file.py +18 -0
  55. querri/types/key.py +36 -0
  56. querri/types/policy.py +105 -0
  57. querri/types/project.py +158 -0
  58. querri/types/sharing.py +19 -0
  59. querri/types/usage.py +39 -0
  60. querri/types/user.py +33 -0
  61. querri-1.0.2.dist-info/METADATA +310 -0
  62. querri-1.0.2.dist-info/RECORD +64 -0
  63. querri-1.0.2.dist-info/WHEEL +4 -0
  64. querri-1.0.2.dist-info/entry_points.txt +2 -0
querri/_exceptions.py ADDED
@@ -0,0 +1,180 @@
1
+ """Exception hierarchy for the Querri SDK.
2
+
3
+ Maps Stripe-style API error responses to typed Python exceptions.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+
9
+ class QuerriError(Exception):
10
+ """Base exception for all SDK errors."""
11
+
12
+ def __init__(self, message: str) -> None:
13
+ super().__init__(message)
14
+ self.message = message
15
+
16
+
17
+ class APIError(QuerriError):
18
+ """HTTP error from the Querri API.
19
+
20
+ Attributes:
21
+ status: HTTP status code.
22
+ type: Error type from the API response (e.g., "authentication_error").
23
+ code: Error code from the API response (e.g., "invalid_api_key").
24
+ doc_url: Link to documentation about this error.
25
+ request_id: Unique request identifier for support.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ message: str,
31
+ *,
32
+ status: int,
33
+ type: str | None = None,
34
+ code: str | None = None,
35
+ doc_url: str | None = None,
36
+ request_id: str | None = None,
37
+ ) -> None:
38
+ super().__init__(message)
39
+ self.status = status
40
+ self.type = type
41
+ self.code = code
42
+ self.doc_url = doc_url
43
+ self.request_id = request_id
44
+
45
+ def __repr__(self) -> str:
46
+ return (
47
+ f"{self.__class__.__name__}("
48
+ f"status={self.status}, "
49
+ f"type={self.type!r}, "
50
+ f"code={self.code!r}, "
51
+ f"message={self.message!r}"
52
+ f")"
53
+ )
54
+
55
+
56
+ class AuthenticationError(APIError):
57
+ """401 — Invalid or expired API key."""
58
+
59
+
60
+ class PermissionError(APIError):
61
+ """403 — Insufficient scope or access denied."""
62
+
63
+
64
+ class NotFoundError(APIError):
65
+ """404 — Resource not found."""
66
+
67
+
68
+ class ValidationError(APIError):
69
+ """400 — Bad request parameters."""
70
+
71
+
72
+ class ConflictError(APIError):
73
+ """409 — Duplicate resource or conflict."""
74
+
75
+
76
+ class RateLimitError(APIError):
77
+ """429 — Rate limited.
78
+
79
+ Attributes:
80
+ retry_after: Seconds to wait before retrying (from Retry-After header).
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ message: str,
86
+ *,
87
+ status: int = 429,
88
+ retry_after: float | None = None,
89
+ **kwargs: object,
90
+ ) -> None:
91
+ # Extract only the kwargs that APIError accepts
92
+ api_kwargs = {
93
+ k: v
94
+ for k, v in kwargs.items()
95
+ if k in ("type", "code", "doc_url", "request_id")
96
+ }
97
+ super().__init__(message, status=status, **api_kwargs) # type: ignore[arg-type]
98
+ self.retry_after = retry_after
99
+
100
+
101
+ class ServerError(APIError):
102
+ """500+ — Internal server error."""
103
+
104
+
105
+ class StreamError(QuerriError):
106
+ """SSE stream issues."""
107
+
108
+
109
+ class StreamTimeoutError(StreamError):
110
+ """Stream didn't produce data within the expected time."""
111
+
112
+
113
+ class StreamCancelledError(StreamError):
114
+ """Stream was cancelled."""
115
+
116
+
117
+ class ConfigError(QuerriError):
118
+ """Missing API key, invalid configuration, etc."""
119
+
120
+
121
+ # Maps HTTP status codes to exception subclasses. Codes not listed here
122
+ # fall through to the base ``APIError``. Multiple 5xx codes share ``ServerError``.
123
+ _STATUS_MAP: dict[int, type[APIError]] = {
124
+ 400: ValidationError,
125
+ 401: AuthenticationError,
126
+ 403: PermissionError,
127
+ 404: NotFoundError,
128
+ 409: ConflictError,
129
+ 429: RateLimitError,
130
+ 500: ServerError,
131
+ 502: ServerError,
132
+ 503: ServerError,
133
+ }
134
+
135
+
136
+ def raise_for_status(
137
+ status: int,
138
+ body: dict[str, object],
139
+ *,
140
+ request_id: str | None = None,
141
+ retry_after: float | None = None,
142
+ ) -> None:
143
+ """Raise the appropriate exception for an error response.
144
+
145
+ Parses Stripe-style error response bodies of the form
146
+ ``{"error": {"type": ..., "code": ..., "message": ...}}``.
147
+ Falls back to a generic message if the body format doesn't match.
148
+
149
+ Args:
150
+ status: HTTP status code.
151
+ body: Parsed JSON response body (expects Stripe-style {"error": {...}}).
152
+ request_id: Request ID from response headers.
153
+ retry_after: Retry-After value from response headers.
154
+ """
155
+ error = body.get("error", {})
156
+ if isinstance(error, dict):
157
+ err_type = str(error.get("type", ""))
158
+ err_code = str(error.get("code", ""))
159
+ err_message = str(error.get("message", f"HTTP {status}"))
160
+ err_doc_url = error.get("doc_url")
161
+ else:
162
+ err_type = ""
163
+ err_code = ""
164
+ err_message = str(body) if body else f"HTTP {status}"
165
+ err_doc_url = None
166
+
167
+ exc_class = _STATUS_MAP.get(status, APIError)
168
+
169
+ kwargs: dict[str, object] = {
170
+ "status": status,
171
+ "type": err_type or None,
172
+ "code": err_code or None,
173
+ "doc_url": str(err_doc_url) if err_doc_url else None,
174
+ "request_id": request_id,
175
+ }
176
+
177
+ if exc_class is RateLimitError:
178
+ kwargs["retry_after"] = retry_after
179
+
180
+ raise exc_class(err_message, **kwargs) # type: ignore[arg-type]
querri/_pagination.py ADDED
@@ -0,0 +1,278 @@
1
+ """Auto-paginating iterators for list endpoints.
2
+
3
+ Supports both cursor-based (after/has_more) and offset-based (page/page_size)
4
+ pagination patterns transparently.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import AsyncIterator, Iterator
10
+ from typing import Any, Generic, TypeVar
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from ._base_client import AsyncHTTPClient, SyncHTTPClient
15
+
16
+ T = TypeVar("T", bound=BaseModel)
17
+
18
+
19
+ class SyncPage(Generic[T]):
20
+ """A single page of results with metadata."""
21
+
22
+ def __init__(
23
+ self,
24
+ data: list[T],
25
+ has_more: bool,
26
+ next_cursor: str | None = None,
27
+ total: int | None = None,
28
+ ) -> None:
29
+ self.data = data
30
+ self.has_more = has_more
31
+ self.next_cursor = next_cursor
32
+ self.total = total
33
+
34
+ def __len__(self) -> int:
35
+ return len(self.data)
36
+
37
+ def __iter__(self) -> Iterator[T]:
38
+ return iter(self.data)
39
+
40
+
41
+ class SyncCursorPage(Generic[T]):
42
+ """Auto-paginating cursor-based iterator.
43
+
44
+ Usage::
45
+
46
+ for user in client.users.list():
47
+ print(user.email)
48
+
49
+ # Or get a single page
50
+ page = client.users.list(limit=20)
51
+ users = page.data
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ http: SyncHTTPClient,
57
+ path: str,
58
+ model: type[T],
59
+ params: dict[str, Any] | None = None,
60
+ data_key: str = "data",
61
+ ) -> None:
62
+ self._http = http
63
+ self._path = path
64
+ self._model = model
65
+ self._params = params or {}
66
+ self._data_key = data_key
67
+ self._first_page: SyncPage[T] | None = None
68
+
69
+ def _fetch_page(self, params: dict[str, Any]) -> SyncPage[T]:
70
+ """Fetch one page from the API and parse the response body.
71
+
72
+ Handles three response formats:
73
+ 1. Paginated envelope: ``{"data": [...], "has_more": bool, "next_cursor": str}``
74
+ 2. Offset-based fallback: ``{"data": [...], "page": int, "total_pages": int}``
75
+ 3. Plain list: ``[...]`` (internal API returns raw arrays)
76
+
77
+ This lets the same iterator work with both the public and internal APIs.
78
+ """
79
+ response = self._http.get(self._path, params=params)
80
+ body = response.json()
81
+
82
+ # Internal API may return a plain list instead of a paginated envelope
83
+ if isinstance(body, list):
84
+ items = [self._model.model_validate(item) for item in body]
85
+ return SyncPage(data=items, has_more=False)
86
+
87
+ items_raw = body.get(self._data_key, [])
88
+ items = [self._model.model_validate(item) for item in items_raw]
89
+
90
+ has_more = body.get("has_more", False)
91
+ next_cursor = body.get("next_cursor")
92
+
93
+ # Offset-based fallback
94
+ if next_cursor is None and not has_more:
95
+ page_num = body.get("page")
96
+ total_pages = body.get("total_pages")
97
+ if page_num is not None and total_pages is not None:
98
+ has_more = page_num < total_pages
99
+ if has_more:
100
+ next_cursor = str(page_num + 1)
101
+
102
+ total = body.get("total")
103
+
104
+ return SyncPage(
105
+ data=items,
106
+ has_more=has_more,
107
+ next_cursor=next_cursor,
108
+ total=total,
109
+ )
110
+
111
+ def _ensure_first_page(self) -> SyncPage[T]:
112
+ """Lazy-fetch and cache the first page.
113
+
114
+ Subsequent calls return the cached page.
115
+ """
116
+ if self._first_page is None:
117
+ self._first_page = self._fetch_page(self._params)
118
+ return self._first_page
119
+
120
+ @property
121
+ def data(self) -> list[T]:
122
+ """Items from the first page."""
123
+ return self._ensure_first_page().data
124
+
125
+ @property
126
+ def has_more(self) -> bool:
127
+ """Whether more pages exist beyond the current one."""
128
+ return self._ensure_first_page().has_more
129
+
130
+ @property
131
+ def next_cursor(self) -> str | None:
132
+ """Opaque cursor for fetching the next page, or None."""
133
+ return self._ensure_first_page().next_cursor
134
+
135
+ def first(self) -> T | None:
136
+ """Get the first item, or None if empty."""
137
+ data = self.data
138
+ return data[0] if data else None
139
+
140
+ def to_list(self) -> list[T]:
141
+ """Consume all pages and return items as a flat list."""
142
+ return list(self)
143
+
144
+ def __iter__(self) -> Iterator[T]:
145
+ """Auto-paginate through all results."""
146
+ page = self._ensure_first_page()
147
+ yield from page.data
148
+
149
+ while page.has_more and page.next_cursor:
150
+ # "after" is the standard cursor param name across all Querri list endpoints
151
+ params = {**self._params, "after": page.next_cursor}
152
+ page = self._fetch_page(params)
153
+ yield from page.data
154
+
155
+
156
+ class AsyncPage(Generic[T]):
157
+ """A single async page of results."""
158
+
159
+ def __init__(
160
+ self,
161
+ data: list[T],
162
+ has_more: bool,
163
+ next_cursor: str | None = None,
164
+ total: int | None = None,
165
+ ) -> None:
166
+ self.data = data
167
+ self.has_more = has_more
168
+ self.next_cursor = next_cursor
169
+ self.total = total
170
+
171
+
172
+ class AsyncCursorPage(Generic[T]):
173
+ """Auto-paginating async cursor-based iterator.
174
+
175
+ Usage::
176
+
177
+ async for user in client.users.list():
178
+ print(user.email)
179
+
180
+ # Or get a single page
181
+ page = client.users.list(limit=20)
182
+ users = await page.get_data()
183
+ """
184
+
185
+ def __init__(
186
+ self,
187
+ http: AsyncHTTPClient,
188
+ path: str,
189
+ model: type[T],
190
+ params: dict[str, Any] | None = None,
191
+ data_key: str = "data",
192
+ ) -> None:
193
+ self._http = http
194
+ self._path = path
195
+ self._model = model
196
+ self._params = params or {}
197
+ self._data_key = data_key
198
+ self._first_page: AsyncPage[T] | None = None
199
+
200
+ async def _fetch_page(self, params: dict[str, Any]) -> AsyncPage[T]:
201
+ """Fetch one page from the API and parse the response body.
202
+
203
+ Handles three response formats:
204
+ 1. Paginated envelope: ``{"data": [...], "has_more": bool, "next_cursor": str}``
205
+ 2. Offset-based fallback: ``{"data": [...], "page": int, "total_pages": int}``
206
+ 3. Plain list: ``[...]`` (internal API returns raw arrays)
207
+
208
+ This lets the same iterator work with both the public and internal APIs.
209
+ """
210
+ response = await self._http.get(self._path, params=params)
211
+ body = response.json()
212
+
213
+ # Internal API may return a plain list instead of a paginated envelope
214
+ if isinstance(body, list):
215
+ items = [self._model.model_validate(item) for item in body]
216
+ return AsyncPage(data=items, has_more=False)
217
+
218
+ items_raw = body.get(self._data_key, [])
219
+ items = [self._model.model_validate(item) for item in items_raw]
220
+
221
+ has_more = body.get("has_more", False)
222
+ next_cursor = body.get("next_cursor")
223
+
224
+ if next_cursor is None and not has_more:
225
+ page_num = body.get("page")
226
+ total_pages = body.get("total_pages")
227
+ if page_num is not None and total_pages is not None:
228
+ has_more = page_num < total_pages
229
+ if has_more:
230
+ next_cursor = str(page_num + 1)
231
+
232
+ total = body.get("total")
233
+
234
+ return AsyncPage(
235
+ data=items,
236
+ has_more=has_more,
237
+ next_cursor=next_cursor,
238
+ total=total,
239
+ )
240
+
241
+ async def _ensure_first_page(self) -> AsyncPage[T]:
242
+ """Lazy-fetch and cache the first page.
243
+
244
+ Subsequent calls return the cached page.
245
+ """
246
+ if self._first_page is None:
247
+ self._first_page = await self._fetch_page(self._params)
248
+ return self._first_page
249
+
250
+ async def get_data(self) -> list[T]:
251
+ """Items from the first page.
252
+
253
+ Unlike ``SyncCursorPage.data`` (a property), this must be awaited::
254
+
255
+ data = await page.get_data()
256
+ """
257
+ page = await self._ensure_first_page()
258
+ return page.data
259
+
260
+ async def first(self) -> T | None:
261
+ page = await self._ensure_first_page()
262
+ return page.data[0] if page.data else None
263
+
264
+ async def to_list(self) -> list[T]:
265
+ """Consume all pages and return items as a flat list."""
266
+ return [item async for item in self]
267
+
268
+ async def __aiter__(self) -> AsyncIterator[T]:
269
+ page = await self._ensure_first_page()
270
+ for item in page.data:
271
+ yield item
272
+
273
+ while page.has_more and page.next_cursor:
274
+ # "after" is the standard cursor param name across all Querri list endpoints
275
+ params = {**self._params, "after": page.next_cursor}
276
+ page = await self._fetch_page(params)
277
+ for item in page.data:
278
+ yield item