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/_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
+ )