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.
- querri/__init__.py +51 -0
- querri/_auth.py +818 -0
- querri/_base_client.py +379 -0
- querri/_client.py +395 -0
- querri/_config.py +140 -0
- querri/_convenience.py +538 -0
- querri/_exceptions.py +180 -0
- querri/_pagination.py +278 -0
- querri/_streaming.py +663 -0
- querri/_user_client.py +193 -0
- querri/_version.py +1 -0
- querri/cli/__init__.py +21 -0
- querri/cli/_app.py +246 -0
- querri/cli/_context.py +216 -0
- querri/cli/_image.py +234 -0
- querri/cli/_output.py +293 -0
- querri/cli/audit.py +68 -0
- querri/cli/auth.py +556 -0
- querri/cli/chat.py +1255 -0
- querri/cli/chats.py +358 -0
- querri/cli/dashboards.py +294 -0
- querri/cli/embed.py +220 -0
- querri/cli/files.py +214 -0
- querri/cli/keys.py +304 -0
- querri/cli/policies.py +424 -0
- querri/cli/projects.py +916 -0
- querri/cli/sharing.py +403 -0
- querri/cli/sources.py +532 -0
- querri/cli/steps.py +123 -0
- querri/cli/usage.py +99 -0
- querri/cli/users.py +223 -0
- querri/cli/views.py +449 -0
- querri/cli/whoami.py +78 -0
- querri/py.typed +0 -0
- querri/resources/__init__.py +1 -0
- querri/resources/audit.py +119 -0
- querri/resources/dashboards.py +295 -0
- querri/resources/embed.py +268 -0
- querri/resources/files.py +145 -0
- querri/resources/keys.py +191 -0
- querri/resources/policies.py +492 -0
- querri/resources/projects.py +808 -0
- querri/resources/sharing.py +330 -0
- querri/resources/sources.py +511 -0
- querri/resources/usage.py +106 -0
- querri/resources/users.py +303 -0
- querri/resources/views.py +347 -0
- querri/types/__init__.py +104 -0
- querri/types/audit.py +21 -0
- querri/types/chat.py +51 -0
- querri/types/dashboard.py +51 -0
- querri/types/data.py +61 -0
- querri/types/embed.py +38 -0
- querri/types/file.py +18 -0
- querri/types/key.py +36 -0
- querri/types/policy.py +105 -0
- querri/types/project.py +158 -0
- querri/types/sharing.py +19 -0
- querri/types/usage.py +39 -0
- querri/types/user.py +33 -0
- querri-1.0.2.dist-info/METADATA +310 -0
- querri-1.0.2.dist-info/RECORD +64 -0
- querri-1.0.2.dist-info/WHEEL +4 -0
- querri-1.0.2.dist-info/entry_points.txt +2 -0
querri/_client.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Public client classes: Querri (sync) and AsyncQuerri (async)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from ._base_client import AsyncHTTPClient, SyncHTTPClient
|
|
8
|
+
from ._config import resolve_config
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ._user_client import AsyncUserQuerri, UserQuerri
|
|
12
|
+
from .resources.audit import AsyncAudit, Audit
|
|
13
|
+
from .resources.dashboards import AsyncDashboards, Dashboards
|
|
14
|
+
from .resources.embed import AsyncEmbed, Embed
|
|
15
|
+
from .resources.files import AsyncFiles, Files
|
|
16
|
+
from .resources.keys import AsyncKeys, Keys
|
|
17
|
+
from .resources.policies import AsyncPolicies, Policies
|
|
18
|
+
from .resources.projects import AsyncProjects, Projects
|
|
19
|
+
from .resources.sharing import AsyncSharing, Sharing
|
|
20
|
+
from .resources.sources import AsyncSources, Sources
|
|
21
|
+
from .resources.usage import AsyncUsage, Usage
|
|
22
|
+
from .resources.users import AsyncUsers, Users
|
|
23
|
+
from .resources.views import AsyncViews, Views
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Querri:
|
|
27
|
+
"""Synchronous Querri API client.
|
|
28
|
+
|
|
29
|
+
Usage::
|
|
30
|
+
|
|
31
|
+
from querri import Querri
|
|
32
|
+
|
|
33
|
+
client = Querri(api_key="qk_...", org_id="org_...")
|
|
34
|
+
for user in client.users.list():
|
|
35
|
+
print(user.email)
|
|
36
|
+
|
|
37
|
+
Or from environment variables::
|
|
38
|
+
|
|
39
|
+
client = Querri() # reads QUERRI_API_KEY, QUERRI_ORG_ID
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
api_key: str | None = None,
|
|
46
|
+
access_token: str | None = None,
|
|
47
|
+
org_id: str | None = None,
|
|
48
|
+
host: str | None = None,
|
|
49
|
+
timeout: float | None = None,
|
|
50
|
+
max_retries: int | None = None,
|
|
51
|
+
profile: str | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize the synchronous Querri client.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
api_key: API key (``qk_`` prefix). Falls back to ``QUERRI_API_KEY`` env var.
|
|
57
|
+
access_token: JWT access token. Falls back to
|
|
58
|
+
``QUERRI_ACCESS_TOKEN`` env var.
|
|
59
|
+
org_id: Organization ID. Falls back to ``QUERRI_ORG_ID`` env var.
|
|
60
|
+
host: API host URL. Falls back to ``QUERRI_HOST`` (default: ``https://app.querri.com``).
|
|
61
|
+
timeout: Request timeout in seconds (default: 30.0).
|
|
62
|
+
max_retries: Max retry attempts for failed requests (default: 3).
|
|
63
|
+
profile: Named auth profile for token store selection.
|
|
64
|
+
"""
|
|
65
|
+
self._profile = profile
|
|
66
|
+
self._config = resolve_config(
|
|
67
|
+
api_key=api_key,
|
|
68
|
+
access_token=access_token,
|
|
69
|
+
org_id=org_id,
|
|
70
|
+
host=host,
|
|
71
|
+
timeout=timeout,
|
|
72
|
+
max_retries=max_retries,
|
|
73
|
+
)
|
|
74
|
+
self._http = SyncHTTPClient(self._config)
|
|
75
|
+
|
|
76
|
+
# Resource namespaces — lazily initialized on first access.
|
|
77
|
+
# Deferred imports keep client creation fast and avoid circular imports.
|
|
78
|
+
self._users: object | None = None
|
|
79
|
+
self._embed: object | None = None
|
|
80
|
+
self._policies: object | None = None
|
|
81
|
+
self._projects: object | None = None
|
|
82
|
+
self._dashboards: object | None = None
|
|
83
|
+
self._files: object | None = None
|
|
84
|
+
self._sources: object | None = None
|
|
85
|
+
self._views: object | None = None
|
|
86
|
+
self._keys: object | None = None
|
|
87
|
+
self._sharing: object | None = None
|
|
88
|
+
self._usage: object | None = None
|
|
89
|
+
self._audit: object | None = None
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def users(self) -> "Users":
|
|
93
|
+
if self._users is None:
|
|
94
|
+
from .resources.users import Users
|
|
95
|
+
|
|
96
|
+
self._users = Users(self._http)
|
|
97
|
+
return self._users # type: ignore[return-value]
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def embed(self) -> "Embed":
|
|
101
|
+
if self._embed is None:
|
|
102
|
+
from .resources.embed import Embed
|
|
103
|
+
|
|
104
|
+
self._embed = Embed(self._http)
|
|
105
|
+
return self._embed # type: ignore[return-value]
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def policies(self) -> "Policies":
|
|
109
|
+
if self._policies is None:
|
|
110
|
+
from .resources.policies import Policies
|
|
111
|
+
|
|
112
|
+
self._policies = Policies(self._http)
|
|
113
|
+
return self._policies # type: ignore[return-value]
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def projects(self) -> "Projects":
|
|
117
|
+
if self._projects is None:
|
|
118
|
+
from .resources.projects import Projects
|
|
119
|
+
|
|
120
|
+
self._projects = Projects(self._http)
|
|
121
|
+
return self._projects # type: ignore[return-value]
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def dashboards(self) -> "Dashboards":
|
|
125
|
+
if self._dashboards is None:
|
|
126
|
+
from .resources.dashboards import Dashboards
|
|
127
|
+
|
|
128
|
+
self._dashboards = Dashboards(self._http)
|
|
129
|
+
return self._dashboards # type: ignore[return-value]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def files(self) -> "Files":
|
|
133
|
+
if self._files is None:
|
|
134
|
+
from .resources.files import Files
|
|
135
|
+
|
|
136
|
+
self._files = Files(self._http)
|
|
137
|
+
return self._files # type: ignore[return-value]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def sources(self) -> "Sources":
|
|
141
|
+
if self._sources is None:
|
|
142
|
+
from .resources.sources import Sources
|
|
143
|
+
|
|
144
|
+
self._sources = Sources(self._http)
|
|
145
|
+
return self._sources # type: ignore[return-value]
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def views(self) -> "Views":
|
|
149
|
+
if self._views is None:
|
|
150
|
+
from .resources.views import Views
|
|
151
|
+
|
|
152
|
+
self._views = Views(self._http)
|
|
153
|
+
return self._views # type: ignore[return-value]
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def keys(self) -> "Keys":
|
|
157
|
+
if self._keys is None:
|
|
158
|
+
from .resources.keys import Keys
|
|
159
|
+
|
|
160
|
+
self._keys = Keys(self._http)
|
|
161
|
+
return self._keys # type: ignore[return-value]
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def sharing(self) -> "Sharing":
|
|
165
|
+
if self._sharing is None:
|
|
166
|
+
from .resources.sharing import Sharing
|
|
167
|
+
|
|
168
|
+
self._sharing = Sharing(self._http)
|
|
169
|
+
return self._sharing # type: ignore[return-value]
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def usage(self) -> "Usage":
|
|
173
|
+
if self._usage is None:
|
|
174
|
+
from .resources.usage import Usage
|
|
175
|
+
|
|
176
|
+
self._usage = Usage(self._http)
|
|
177
|
+
return self._usage # type: ignore[return-value]
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def audit(self) -> "Audit":
|
|
181
|
+
if self._audit is None:
|
|
182
|
+
from .resources.audit import Audit
|
|
183
|
+
|
|
184
|
+
self._audit = Audit(self._http)
|
|
185
|
+
return self._audit # type: ignore[return-value]
|
|
186
|
+
|
|
187
|
+
def as_user(self, session: dict[str, Any]) -> "UserQuerri":
|
|
188
|
+
"""Create a user-scoped client from a ``get_session()`` result.
|
|
189
|
+
|
|
190
|
+
The returned client uses the embed session token for auth and
|
|
191
|
+
only exposes resources visible to the user (projects, dashboards,
|
|
192
|
+
sources, chats). All queries are automatically filtered
|
|
193
|
+
by the user's access policies.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
session: Result from ``get_session()`` containing ``session_token``.
|
|
197
|
+
"""
|
|
198
|
+
from ._user_client import UserQuerri
|
|
199
|
+
|
|
200
|
+
return UserQuerri(session, self._config)
|
|
201
|
+
|
|
202
|
+
def close(self) -> None:
|
|
203
|
+
"""Close the underlying HTTP client."""
|
|
204
|
+
self._http.close()
|
|
205
|
+
|
|
206
|
+
def __enter__(self) -> "Querri":
|
|
207
|
+
"""Enter context manager for automatic resource cleanup."""
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def __exit__(self, *args: object) -> None:
|
|
211
|
+
"""Close the HTTP client on context manager exit."""
|
|
212
|
+
self.close()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class AsyncQuerri:
|
|
216
|
+
"""Asynchronous Querri API client.
|
|
217
|
+
|
|
218
|
+
Usage::
|
|
219
|
+
|
|
220
|
+
from querri import AsyncQuerri
|
|
221
|
+
|
|
222
|
+
async with AsyncQuerri() as client:
|
|
223
|
+
async for user in client.users.list():
|
|
224
|
+
print(user.email)
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
*,
|
|
230
|
+
api_key: str | None = None,
|
|
231
|
+
access_token: str | None = None,
|
|
232
|
+
org_id: str | None = None,
|
|
233
|
+
host: str | None = None,
|
|
234
|
+
timeout: float | None = None,
|
|
235
|
+
max_retries: int | None = None,
|
|
236
|
+
profile: str | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Initialize the asynchronous Querri client.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
api_key: API key (``qk_`` prefix). Falls back to ``QUERRI_API_KEY`` env var.
|
|
242
|
+
access_token: JWT access token. Falls back to
|
|
243
|
+
``QUERRI_ACCESS_TOKEN`` env var.
|
|
244
|
+
org_id: Organization ID. Falls back to ``QUERRI_ORG_ID`` env var.
|
|
245
|
+
host: API host URL. Falls back to ``QUERRI_HOST`` (default: ``https://app.querri.com``).
|
|
246
|
+
timeout: Request timeout in seconds (default: 30.0).
|
|
247
|
+
max_retries: Max retry attempts for failed requests (default: 3).
|
|
248
|
+
profile: Named auth profile for token store selection.
|
|
249
|
+
"""
|
|
250
|
+
self._profile = profile
|
|
251
|
+
self._config = resolve_config(
|
|
252
|
+
api_key=api_key,
|
|
253
|
+
access_token=access_token,
|
|
254
|
+
org_id=org_id,
|
|
255
|
+
host=host,
|
|
256
|
+
timeout=timeout,
|
|
257
|
+
max_retries=max_retries,
|
|
258
|
+
)
|
|
259
|
+
self._http = AsyncHTTPClient(self._config)
|
|
260
|
+
|
|
261
|
+
self._users: object | None = None
|
|
262
|
+
self._embed: object | None = None
|
|
263
|
+
self._policies: object | None = None
|
|
264
|
+
self._projects: object | None = None
|
|
265
|
+
self._dashboards: object | None = None
|
|
266
|
+
self._files: object | None = None
|
|
267
|
+
self._sources: object | None = None
|
|
268
|
+
self._views: object | None = None
|
|
269
|
+
self._keys: object | None = None
|
|
270
|
+
self._sharing: object | None = None
|
|
271
|
+
self._usage: object | None = None
|
|
272
|
+
self._audit: object | None = None
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def users(self) -> "AsyncUsers":
|
|
276
|
+
if self._users is None:
|
|
277
|
+
from .resources.users import AsyncUsers
|
|
278
|
+
|
|
279
|
+
self._users = AsyncUsers(self._http)
|
|
280
|
+
return self._users # type: ignore[return-value]
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def embed(self) -> "AsyncEmbed":
|
|
284
|
+
if self._embed is None:
|
|
285
|
+
from .resources.embed import AsyncEmbed
|
|
286
|
+
|
|
287
|
+
self._embed = AsyncEmbed(self._http)
|
|
288
|
+
return self._embed # type: ignore[return-value]
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def policies(self) -> "AsyncPolicies":
|
|
292
|
+
if self._policies is None:
|
|
293
|
+
from .resources.policies import AsyncPolicies
|
|
294
|
+
|
|
295
|
+
self._policies = AsyncPolicies(self._http)
|
|
296
|
+
return self._policies # type: ignore[return-value]
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def projects(self) -> "AsyncProjects":
|
|
300
|
+
if self._projects is None:
|
|
301
|
+
from .resources.projects import AsyncProjects
|
|
302
|
+
|
|
303
|
+
self._projects = AsyncProjects(self._http)
|
|
304
|
+
return self._projects # type: ignore[return-value]
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def dashboards(self) -> "AsyncDashboards":
|
|
308
|
+
if self._dashboards is None:
|
|
309
|
+
from .resources.dashboards import AsyncDashboards
|
|
310
|
+
|
|
311
|
+
self._dashboards = AsyncDashboards(self._http)
|
|
312
|
+
return self._dashboards # type: ignore[return-value]
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def files(self) -> "AsyncFiles":
|
|
316
|
+
if self._files is None:
|
|
317
|
+
from .resources.files import AsyncFiles
|
|
318
|
+
|
|
319
|
+
self._files = AsyncFiles(self._http)
|
|
320
|
+
return self._files # type: ignore[return-value]
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def sources(self) -> "AsyncSources":
|
|
324
|
+
if self._sources is None:
|
|
325
|
+
from .resources.sources import AsyncSources
|
|
326
|
+
|
|
327
|
+
self._sources = AsyncSources(self._http)
|
|
328
|
+
return self._sources # type: ignore[return-value]
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def views(self) -> "AsyncViews":
|
|
332
|
+
if self._views is None:
|
|
333
|
+
from .resources.views import AsyncViews
|
|
334
|
+
|
|
335
|
+
self._views = AsyncViews(self._http)
|
|
336
|
+
return self._views # type: ignore[return-value]
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def keys(self) -> "AsyncKeys":
|
|
340
|
+
if self._keys is None:
|
|
341
|
+
from .resources.keys import AsyncKeys
|
|
342
|
+
|
|
343
|
+
self._keys = AsyncKeys(self._http)
|
|
344
|
+
return self._keys # type: ignore[return-value]
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def sharing(self) -> "AsyncSharing":
|
|
348
|
+
if self._sharing is None:
|
|
349
|
+
from .resources.sharing import AsyncSharing
|
|
350
|
+
|
|
351
|
+
self._sharing = AsyncSharing(self._http)
|
|
352
|
+
return self._sharing # type: ignore[return-value]
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def usage(self) -> "AsyncUsage":
|
|
356
|
+
if self._usage is None:
|
|
357
|
+
from .resources.usage import AsyncUsage
|
|
358
|
+
|
|
359
|
+
self._usage = AsyncUsage(self._http)
|
|
360
|
+
return self._usage # type: ignore[return-value]
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def audit(self) -> "AsyncAudit":
|
|
364
|
+
if self._audit is None:
|
|
365
|
+
from .resources.audit import AsyncAudit
|
|
366
|
+
|
|
367
|
+
self._audit = AsyncAudit(self._http)
|
|
368
|
+
return self._audit # type: ignore[return-value]
|
|
369
|
+
|
|
370
|
+
def as_user(self, session: dict[str, Any]) -> "AsyncUserQuerri":
|
|
371
|
+
"""Create a user-scoped async client from a ``get_session()`` result.
|
|
372
|
+
|
|
373
|
+
The returned client uses the embed session token for auth and
|
|
374
|
+
only exposes resources visible to the user (projects, dashboards,
|
|
375
|
+
sources, chats). All queries are automatically filtered
|
|
376
|
+
by the user's access policies.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
session: Result from ``get_session()`` containing ``session_token``.
|
|
380
|
+
"""
|
|
381
|
+
from ._user_client import AsyncUserQuerri
|
|
382
|
+
|
|
383
|
+
return AsyncUserQuerri(session, self._config)
|
|
384
|
+
|
|
385
|
+
async def close(self) -> None:
|
|
386
|
+
"""Close the underlying HTTP client."""
|
|
387
|
+
await self._http.close()
|
|
388
|
+
|
|
389
|
+
async def __aenter__(self) -> "AsyncQuerri":
|
|
390
|
+
"""Enter async context manager for automatic resource cleanup."""
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
async def __aexit__(self, *args: object) -> None:
|
|
394
|
+
"""Close the HTTP client on async context manager exit."""
|
|
395
|
+
await self.close()
|
querri/_config.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Configuration and defaults for the Querri SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from ._exceptions import ConfigError
|
|
9
|
+
|
|
10
|
+
DEFAULT_HOST = "https://app.querri.com" #: Production API host.
|
|
11
|
+
DEFAULT_TIMEOUT = 30.0 #: Request timeout in seconds.
|
|
12
|
+
DEFAULT_MAX_RETRIES = 3 #: Max retry attempts for retryable errors.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ClientConfig:
|
|
17
|
+
"""Resolved client configuration.
|
|
18
|
+
|
|
19
|
+
Values are resolved in priority order:
|
|
20
|
+
1. Explicit constructor arguments
|
|
21
|
+
2. Environment variables
|
|
22
|
+
3. Defaults
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
api_key: str | None = None #: Querri API key (``qk_`` prefix).
|
|
26
|
+
access_token: str | None = None #: JWT access token (from OAuth or env).
|
|
27
|
+
org_id: str | None = None #: Organization ID for tenant isolation.
|
|
28
|
+
base_url: str = DEFAULT_HOST + "/api/v1" #: Fully resolved API base URL.
|
|
29
|
+
timeout: float = DEFAULT_TIMEOUT #: Request timeout in seconds.
|
|
30
|
+
max_retries: int = DEFAULT_MAX_RETRIES #: Max retry attempts for retryable errors.
|
|
31
|
+
session_token: str | None = None #: Embed session token for user-scoped clients.
|
|
32
|
+
_user_agent: str = field(init=False) #: Auto-generated User-Agent header value.
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
"""Build the User-Agent string from the SDK version."""
|
|
36
|
+
from ._version import __version__
|
|
37
|
+
|
|
38
|
+
self._user_agent = f"querri-python/{__version__}"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def user_agent(self) -> str:
|
|
42
|
+
"""The User-Agent header value, e.g. ``querri-python/0.1.0``."""
|
|
43
|
+
return self._user_agent
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
"""Redact secrets to prevent accidental exposure in logs/tracebacks."""
|
|
47
|
+
key_repr = f"qk_***...{self.api_key[-4:]}" if self.api_key else None
|
|
48
|
+
token_repr = "ey***" if self.access_token else None
|
|
49
|
+
return (
|
|
50
|
+
f"ClientConfig(base_url={self.base_url!r}, api_key={key_repr!r}, "
|
|
51
|
+
f"access_token={token_repr!r}, org_id={self.org_id!r})"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return self.__repr__()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_config(
|
|
59
|
+
*,
|
|
60
|
+
api_key: str | None = None,
|
|
61
|
+
access_token: str | None = None,
|
|
62
|
+
org_id: str | None = None,
|
|
63
|
+
host: str | None = None,
|
|
64
|
+
timeout: float | None = None,
|
|
65
|
+
max_retries: int | None = None,
|
|
66
|
+
) -> ClientConfig:
|
|
67
|
+
"""Resolve configuration from arguments, env vars, and defaults.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
api_key: API key (or set QUERRI_API_KEY env var).
|
|
71
|
+
access_token: JWT access token (or set QUERRI_ACCESS_TOKEN env var).
|
|
72
|
+
When provided, org_id is derived from JWT claims.
|
|
73
|
+
org_id: Organization ID (or set QUERRI_ORG_ID env var).
|
|
74
|
+
Required for API key auth; optional for JWT auth.
|
|
75
|
+
host: Server host (or set QUERRI_HOST env var).
|
|
76
|
+
``/api/v1`` is appended automatically.
|
|
77
|
+
Example: ``host="http://localhost"`` becomes
|
|
78
|
+
``http://localhost/api/v1``.
|
|
79
|
+
Defaults to ``https://app.querri.com``.
|
|
80
|
+
timeout: Request timeout in seconds.
|
|
81
|
+
max_retries: Max retry attempts for retryable errors.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ConfigError: If no credentials are provided.
|
|
85
|
+
"""
|
|
86
|
+
resolved_key = api_key or os.environ.get("QUERRI_API_KEY")
|
|
87
|
+
resolved_token = access_token or os.environ.get("QUERRI_ACCESS_TOKEN")
|
|
88
|
+
|
|
89
|
+
# 3rd priority: token store (~/.querri/tokens.json)
|
|
90
|
+
if not resolved_key and not resolved_token:
|
|
91
|
+
try:
|
|
92
|
+
from ._auth import TokenStore
|
|
93
|
+
|
|
94
|
+
store = TokenStore.load()
|
|
95
|
+
profile = store.get_active_profile()
|
|
96
|
+
if profile and profile.access_token:
|
|
97
|
+
resolved_token = profile.access_token
|
|
98
|
+
if not org_id and profile.org_id:
|
|
99
|
+
org_id = profile.org_id
|
|
100
|
+
except Exception:
|
|
101
|
+
# Token store unavailable — continue to error
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
if not resolved_key and not resolved_token:
|
|
105
|
+
raise ConfigError(
|
|
106
|
+
"No credentials found. Pass api_key= to the constructor, "
|
|
107
|
+
"set the QUERRI_API_KEY environment variable, or run "
|
|
108
|
+
"'querri auth login'."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
resolved_org = org_id or os.environ.get("QUERRI_ORG_ID")
|
|
112
|
+
|
|
113
|
+
# org_id is required for API key auth, optional for JWT auth
|
|
114
|
+
if resolved_key and not resolved_token and not resolved_org:
|
|
115
|
+
raise ConfigError(
|
|
116
|
+
"No organization ID provided. Pass org_id= to the constructor "
|
|
117
|
+
"or set the QUERRI_ORG_ID environment variable."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
resolved_host = host or os.environ.get("QUERRI_HOST") or DEFAULT_HOST
|
|
121
|
+
resolved_url = resolved_host.rstrip("/") + "/api/v1"
|
|
122
|
+
|
|
123
|
+
resolved_timeout = timeout
|
|
124
|
+
if resolved_timeout is None:
|
|
125
|
+
env_timeout = os.environ.get("QUERRI_TIMEOUT")
|
|
126
|
+
resolved_timeout = float(env_timeout) if env_timeout else DEFAULT_TIMEOUT
|
|
127
|
+
|
|
128
|
+
resolved_retries = max_retries
|
|
129
|
+
if resolved_retries is None:
|
|
130
|
+
env_retries = os.environ.get("QUERRI_MAX_RETRIES")
|
|
131
|
+
resolved_retries = int(env_retries) if env_retries else DEFAULT_MAX_RETRIES
|
|
132
|
+
|
|
133
|
+
return ClientConfig(
|
|
134
|
+
api_key=resolved_key,
|
|
135
|
+
access_token=resolved_token,
|
|
136
|
+
org_id=resolved_org,
|
|
137
|
+
base_url=resolved_url,
|
|
138
|
+
timeout=resolved_timeout,
|
|
139
|
+
max_retries=resolved_retries,
|
|
140
|
+
)
|