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/_user_client.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""User-scoped client that uses embed session auth and FGA-filtered resources.
|
|
2
|
+
|
|
3
|
+
Calls the internal API (``/api``) instead of the public API (``/api/v1``).
|
|
4
|
+
Resources are automatically filtered by the session user's access policies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from ._base_client import AsyncHTTPClient, SyncHTTPClient
|
|
13
|
+
from ._config import ClientConfig
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .resources.dashboards import AsyncDashboards, Dashboards
|
|
17
|
+
from .resources.projects import AsyncChats, AsyncProjects, Chats, Projects
|
|
18
|
+
from .resources.sources import AsyncSources, Sources
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _session_config(
|
|
22
|
+
session: dict[str, Any], parent_config: ClientConfig
|
|
23
|
+
) -> ClientConfig:
|
|
24
|
+
"""Build a config for session-mode HTTP clients.
|
|
25
|
+
|
|
26
|
+
Derives the ``/api`` base URL from the parent's ``/api/v1`` URL and
|
|
27
|
+
sets the session token for ``X-Embed-Session`` auth.
|
|
28
|
+
"""
|
|
29
|
+
# Strip /api/v1 suffix to get the host, then append /api
|
|
30
|
+
host = re.sub(r"/api/v1$", "", parent_config.base_url)
|
|
31
|
+
return ClientConfig(
|
|
32
|
+
api_key="", # Not used in session mode.
|
|
33
|
+
org_id="", # Not used in session mode.
|
|
34
|
+
base_url=f"{host}/api",
|
|
35
|
+
timeout=parent_config.timeout,
|
|
36
|
+
max_retries=parent_config.max_retries,
|
|
37
|
+
session_token=session["session_token"],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UserQuerri:
|
|
42
|
+
"""User-scoped synchronous client with FGA-filtered resources.
|
|
43
|
+
|
|
44
|
+
Calls the internal API (``/api``) using an embed session token.
|
|
45
|
+
Only exposes resources visible to the session user.
|
|
46
|
+
|
|
47
|
+
Usage::
|
|
48
|
+
|
|
49
|
+
session = client.embed.get_session(user="ext_123")
|
|
50
|
+
user_client = client.as_user(session)
|
|
51
|
+
for project in user_client.projects.list():
|
|
52
|
+
print(project.name)
|
|
53
|
+
|
|
54
|
+
Create via :meth:`Querri.as_user`.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, session: dict[str, Any], parent_config: ClientConfig) -> None:
|
|
58
|
+
"""Initialize with session token and parent client config.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
session: Result from ``get_session()`` containing ``session_token``.
|
|
62
|
+
parent_config: Config from the parent ``Querri`` client.
|
|
63
|
+
"""
|
|
64
|
+
self._config = _session_config(session, parent_config)
|
|
65
|
+
self._http = SyncHTTPClient(self._config)
|
|
66
|
+
|
|
67
|
+
# Resource namespaces — lazily initialized on first access.
|
|
68
|
+
# Deferred imports keep client creation fast and avoid circular imports.
|
|
69
|
+
self._projects: object | None = None
|
|
70
|
+
self._dashboards: object | None = None
|
|
71
|
+
self._sources: object | None = None
|
|
72
|
+
self._chats: object | None = None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def projects(self) -> Projects:
|
|
76
|
+
if self._projects is None:
|
|
77
|
+
from .resources.projects import Projects
|
|
78
|
+
|
|
79
|
+
self._projects = Projects(self._http)
|
|
80
|
+
return self._projects # type: ignore[return-value]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def dashboards(self) -> Dashboards:
|
|
84
|
+
if self._dashboards is None:
|
|
85
|
+
from .resources.dashboards import Dashboards
|
|
86
|
+
|
|
87
|
+
self._dashboards = Dashboards(self._http)
|
|
88
|
+
return self._dashboards # type: ignore[return-value]
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def sources(self) -> Sources:
|
|
92
|
+
if self._sources is None:
|
|
93
|
+
from .resources.sources import Sources
|
|
94
|
+
|
|
95
|
+
self._sources = Sources(self._http)
|
|
96
|
+
return self._sources # type: ignore[return-value]
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def chats(self) -> Chats:
|
|
100
|
+
if self._chats is None:
|
|
101
|
+
from .resources.projects import Chats
|
|
102
|
+
|
|
103
|
+
self._chats = Chats(self._http)
|
|
104
|
+
return self._chats # type: ignore[return-value]
|
|
105
|
+
|
|
106
|
+
def close(self) -> None:
|
|
107
|
+
"""Close the underlying HTTP client."""
|
|
108
|
+
self._http.close()
|
|
109
|
+
|
|
110
|
+
def __enter__(self) -> UserQuerri:
|
|
111
|
+
"""Enter context manager for automatic resource cleanup."""
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def __exit__(self, *args: object) -> None:
|
|
115
|
+
"""Close the HTTP client on context manager exit."""
|
|
116
|
+
self.close()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AsyncUserQuerri:
|
|
120
|
+
"""User-scoped asynchronous client with FGA-filtered resources.
|
|
121
|
+
|
|
122
|
+
Calls the internal API (``/api``) using an embed session token.
|
|
123
|
+
Only exposes resources visible to the session user.
|
|
124
|
+
|
|
125
|
+
Usage::
|
|
126
|
+
|
|
127
|
+
session = await client.embed.get_session(user="ext_123")
|
|
128
|
+
user_client = client.as_user(session)
|
|
129
|
+
async for project in user_client.projects.list():
|
|
130
|
+
print(project.name)
|
|
131
|
+
|
|
132
|
+
Create via :meth:`AsyncQuerri.as_user`.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, session: dict[str, Any], parent_config: ClientConfig) -> None:
|
|
136
|
+
"""Initialize with session token and parent client config.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
session: Result from ``get_session()`` containing ``session_token``.
|
|
140
|
+
parent_config: Config from the parent ``AsyncQuerri`` client.
|
|
141
|
+
"""
|
|
142
|
+
self._config = _session_config(session, parent_config)
|
|
143
|
+
self._http = AsyncHTTPClient(self._config)
|
|
144
|
+
|
|
145
|
+
# Resource namespaces — lazily initialized on first access.
|
|
146
|
+
self._projects: object | None = None
|
|
147
|
+
self._dashboards: object | None = None
|
|
148
|
+
self._sources: object | None = None
|
|
149
|
+
self._chats: object | None = None
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def projects(self) -> AsyncProjects:
|
|
153
|
+
if self._projects is None:
|
|
154
|
+
from .resources.projects import AsyncProjects
|
|
155
|
+
|
|
156
|
+
self._projects = AsyncProjects(self._http)
|
|
157
|
+
return self._projects # type: ignore[return-value]
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def dashboards(self) -> AsyncDashboards:
|
|
161
|
+
if self._dashboards is None:
|
|
162
|
+
from .resources.dashboards import AsyncDashboards
|
|
163
|
+
|
|
164
|
+
self._dashboards = AsyncDashboards(self._http)
|
|
165
|
+
return self._dashboards # type: ignore[return-value]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def sources(self) -> AsyncSources:
|
|
169
|
+
if self._sources is None:
|
|
170
|
+
from .resources.sources import AsyncSources
|
|
171
|
+
|
|
172
|
+
self._sources = AsyncSources(self._http)
|
|
173
|
+
return self._sources # type: ignore[return-value]
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def chats(self) -> AsyncChats:
|
|
177
|
+
if self._chats is None:
|
|
178
|
+
from .resources.projects import AsyncChats
|
|
179
|
+
|
|
180
|
+
self._chats = AsyncChats(self._http)
|
|
181
|
+
return self._chats # type: ignore[return-value]
|
|
182
|
+
|
|
183
|
+
async def close(self) -> None:
|
|
184
|
+
"""Close the underlying HTTP client."""
|
|
185
|
+
await self._http.close()
|
|
186
|
+
|
|
187
|
+
async def __aenter__(self) -> AsyncUserQuerri:
|
|
188
|
+
"""Enter async context manager for automatic resource cleanup."""
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
async def __aexit__(self, *args: object) -> None:
|
|
192
|
+
"""Close the HTTP client on async context manager exit."""
|
|
193
|
+
await self.close()
|
querri/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
querri/cli/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Querri CLI — command-line interface for the Querri data analysis platform.
|
|
2
|
+
|
|
3
|
+
Entry point guard: if typer is not installed, prints a clean error message
|
|
4
|
+
and exits with code 1. No tracebacks, no file paths, no module names.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def app() -> None:
|
|
11
|
+
"""Entry point registered in pyproject.toml as ``querri = "querri.cli:app"``."""
|
|
12
|
+
try:
|
|
13
|
+
from querri.cli._app import main_app
|
|
14
|
+
|
|
15
|
+
main_app()
|
|
16
|
+
except ImportError:
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
print("Querri CLI requires additional dependencies.", file=sys.stderr)
|
|
20
|
+
print("Install with: pip install 'querri[cli]'", file=sys.stderr)
|
|
21
|
+
sys.exit(1)
|
querri/cli/_app.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Main Typer application with global options and sub-app registration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from querri._version import __version__
|
|
10
|
+
from querri.cli.audit import audit_app
|
|
11
|
+
from querri.cli.auth import auth_app
|
|
12
|
+
from querri.cli.chats import chats_app
|
|
13
|
+
from querri.cli.dashboards import dashboards_app
|
|
14
|
+
from querri.cli.embed import embed_app
|
|
15
|
+
from querri.cli.files import files_app
|
|
16
|
+
from querri.cli.keys import keys_app
|
|
17
|
+
from querri.cli.policies import policies_app
|
|
18
|
+
from querri.cli.projects import projects_app
|
|
19
|
+
from querri.cli.sharing import sharing_app
|
|
20
|
+
from querri.cli.sources import sources_app
|
|
21
|
+
from querri.cli.steps import steps_app
|
|
22
|
+
from querri.cli.usage import usage_app
|
|
23
|
+
from querri.cli.users import users_app
|
|
24
|
+
from querri.cli.views import view_app
|
|
25
|
+
from querri.cli.whoami import whoami_app
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# TTY detection — drives output behavior across the CLI
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
IS_INTERACTIVE = sys.stdout.isatty()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Main app
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
main_app = typer.Typer(
|
|
39
|
+
name="querri",
|
|
40
|
+
help=(
|
|
41
|
+
"[bold #f15a24]Querri CLI[/bold #f15a24] — command-line interface for the "
|
|
42
|
+
"[#f15a24]Querri[/#f15a24] data analysis platform.\n\n"
|
|
43
|
+
"Get started:\n"
|
|
44
|
+
" [#f15a24]querri auth login[/#f15a24]"
|
|
45
|
+
" Authenticate via browser\n"
|
|
46
|
+
" [#f15a24]querri project new[/#f15a24]"
|
|
47
|
+
" Create a new project\n"
|
|
48
|
+
" [#f15a24]querri file upload data.csv[/#f15a24] Upload a data file\n"
|
|
49
|
+
' [#f15a24]querri project chat -m "analyze this"[/#f15a24]'
|
|
50
|
+
' Chat with your data'
|
|
51
|
+
),
|
|
52
|
+
no_args_is_help=True,
|
|
53
|
+
rich_markup_mode="rich",
|
|
54
|
+
pretty_exceptions_enable=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Version callback
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _version_callback(value: bool) -> None:
|
|
64
|
+
if value:
|
|
65
|
+
if IS_INTERACTIVE:
|
|
66
|
+
from rich.console import Console
|
|
67
|
+
|
|
68
|
+
Console(stderr=True).print(
|
|
69
|
+
f"[bold #f15a24]querri[/bold #f15a24] {__version__}"
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
print(f"querri {__version__}")
|
|
73
|
+
raise typer.Exit()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Global options callback
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@main_app.callback()
|
|
82
|
+
def _global_options(
|
|
83
|
+
ctx: typer.Context,
|
|
84
|
+
host: str | None = typer.Option(
|
|
85
|
+
None,
|
|
86
|
+
"--host",
|
|
87
|
+
envvar="QUERRI_HOST",
|
|
88
|
+
help="Querri server host (default: https://app.querri.com).",
|
|
89
|
+
show_default=False,
|
|
90
|
+
),
|
|
91
|
+
api_key: str | None = typer.Option(
|
|
92
|
+
None,
|
|
93
|
+
"--api-key",
|
|
94
|
+
envvar="QUERRI_API_KEY",
|
|
95
|
+
help="API key (prefer QUERRI_API_KEY env var).",
|
|
96
|
+
show_default=False,
|
|
97
|
+
),
|
|
98
|
+
org_id: str | None = typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
"--org-id",
|
|
101
|
+
envvar="QUERRI_ORG_ID",
|
|
102
|
+
help="Organization ID.",
|
|
103
|
+
show_default=False,
|
|
104
|
+
),
|
|
105
|
+
profile: str | None = typer.Option(
|
|
106
|
+
None,
|
|
107
|
+
"--profile",
|
|
108
|
+
help="Named auth profile (v0.2.1+).",
|
|
109
|
+
hidden=True,
|
|
110
|
+
),
|
|
111
|
+
json_output: bool = typer.Option(
|
|
112
|
+
False,
|
|
113
|
+
"--json",
|
|
114
|
+
help="Output as JSON.",
|
|
115
|
+
),
|
|
116
|
+
quiet: bool = typer.Option(
|
|
117
|
+
False,
|
|
118
|
+
"--quiet",
|
|
119
|
+
"-q",
|
|
120
|
+
help="Minimal output (IDs only).",
|
|
121
|
+
),
|
|
122
|
+
verbose: bool = typer.Option(
|
|
123
|
+
False,
|
|
124
|
+
"--verbose",
|
|
125
|
+
"-v",
|
|
126
|
+
help="Verbose output.",
|
|
127
|
+
),
|
|
128
|
+
interactive: bool | None = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--interactive/--no-interactive",
|
|
131
|
+
help="Force interactive/non-interactive mode.",
|
|
132
|
+
show_default=False,
|
|
133
|
+
),
|
|
134
|
+
project: str | None = typer.Option(
|
|
135
|
+
None,
|
|
136
|
+
"--project",
|
|
137
|
+
"-p",
|
|
138
|
+
envvar="QUERRI_PROJECT_ID",
|
|
139
|
+
help="Project ID (overrides active project).",
|
|
140
|
+
show_default=False,
|
|
141
|
+
),
|
|
142
|
+
chat: str | None = typer.Option(
|
|
143
|
+
None,
|
|
144
|
+
"--chat",
|
|
145
|
+
envvar="QUERRI_CHAT_ID",
|
|
146
|
+
help="Chat ID (overrides active chat).",
|
|
147
|
+
show_default=False,
|
|
148
|
+
),
|
|
149
|
+
version: bool = typer.Option(
|
|
150
|
+
False,
|
|
151
|
+
"--version",
|
|
152
|
+
callback=_version_callback,
|
|
153
|
+
is_eager=True,
|
|
154
|
+
help="Show version and exit.",
|
|
155
|
+
),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""[#f15a24]Querri CLI[/#f15a24] — manage projects,
|
|
158
|
+
data, and AI chats from the terminal."""
|
|
159
|
+
# Determine interactive mode
|
|
160
|
+
is_interactive = interactive if interactive is not None else IS_INTERACTIVE
|
|
161
|
+
|
|
162
|
+
# Warn about --api-key in shell history (only in interactive TTY mode)
|
|
163
|
+
if api_key and is_interactive:
|
|
164
|
+
import sys as _sys
|
|
165
|
+
|
|
166
|
+
print(
|
|
167
|
+
"Warning: API key passed as command argument may appear in shell history. "
|
|
168
|
+
"Consider using QUERRI_API_KEY env var instead.",
|
|
169
|
+
file=_sys.stderr,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Store options in context for downstream commands
|
|
173
|
+
ctx.ensure_object(dict)
|
|
174
|
+
ctx.obj["host"] = host
|
|
175
|
+
ctx.obj["api_key"] = api_key
|
|
176
|
+
ctx.obj["org_id"] = org_id
|
|
177
|
+
ctx.obj["profile"] = profile
|
|
178
|
+
ctx.obj["project"] = project
|
|
179
|
+
ctx.obj["chat"] = chat
|
|
180
|
+
ctx.obj["json"] = json_output
|
|
181
|
+
ctx.obj["quiet"] = quiet
|
|
182
|
+
ctx.obj["verbose"] = verbose
|
|
183
|
+
ctx.obj["interactive"] = is_interactive
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Sub-app registration with rich_help_panel groupings
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
# ── Getting Started ──────────────────────────────────────────────────────
|
|
191
|
+
main_app.add_typer(
|
|
192
|
+
auth_app, name="auth", rich_help_panel="[#f15a24]Getting Started[/#f15a24]"
|
|
193
|
+
)
|
|
194
|
+
main_app.add_typer(
|
|
195
|
+
whoami_app, name="whoami", rich_help_panel="[#f15a24]Getting Started[/#f15a24]"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# ── Projects & Data ─────────────────────────────────────────────────────
|
|
199
|
+
main_app.add_typer(
|
|
200
|
+
projects_app, name="project", rich_help_panel="[#f15a24]Projects & Data[/#f15a24]"
|
|
201
|
+
)
|
|
202
|
+
main_app.add_typer(
|
|
203
|
+
chats_app, name="chat", rich_help_panel="[#f15a24]Projects & Data[/#f15a24]"
|
|
204
|
+
)
|
|
205
|
+
main_app.add_typer(
|
|
206
|
+
steps_app, name="step", rich_help_panel="[#f15a24]Projects & Data[/#f15a24]"
|
|
207
|
+
)
|
|
208
|
+
main_app.add_typer(
|
|
209
|
+
files_app, name="file", rich_help_panel="[#f15a24]Projects & Data[/#f15a24]"
|
|
210
|
+
)
|
|
211
|
+
main_app.add_typer(
|
|
212
|
+
sources_app, name="source", rich_help_panel="[#f15a24]Projects & Data[/#f15a24]"
|
|
213
|
+
)
|
|
214
|
+
main_app.add_typer(
|
|
215
|
+
view_app, name="view", rich_help_panel="[#f15a24]Projects & Data[/#f15a24]"
|
|
216
|
+
)
|
|
217
|
+
main_app.add_typer(
|
|
218
|
+
dashboards_app,
|
|
219
|
+
name="dashboard",
|
|
220
|
+
rich_help_panel="[#f15a24]Projects & Data[/#f15a24]",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# ── Administration ──────────────────────────────────────────────────────
|
|
224
|
+
main_app.add_typer(
|
|
225
|
+
users_app, name="user", rich_help_panel="[#f15a24]Administration[/#f15a24]"
|
|
226
|
+
)
|
|
227
|
+
main_app.add_typer(
|
|
228
|
+
keys_app, name="key", rich_help_panel="[#f15a24]Administration[/#f15a24]"
|
|
229
|
+
)
|
|
230
|
+
main_app.add_typer(
|
|
231
|
+
policies_app, name="policy", rich_help_panel="[#f15a24]Administration[/#f15a24]"
|
|
232
|
+
)
|
|
233
|
+
main_app.add_typer(
|
|
234
|
+
sharing_app, name="share", rich_help_panel="[#f15a24]Administration[/#f15a24]"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# ── Advanced ────────────────────────────────────────────────────────────
|
|
238
|
+
main_app.add_typer(
|
|
239
|
+
embed_app, name="session", rich_help_panel="[#f15a24]Advanced[/#f15a24]"
|
|
240
|
+
)
|
|
241
|
+
main_app.add_typer(
|
|
242
|
+
usage_app, name="usage", rich_help_panel="[#f15a24]Advanced[/#f15a24]"
|
|
243
|
+
)
|
|
244
|
+
main_app.add_typer(
|
|
245
|
+
audit_app, name="audit", rich_help_panel="[#f15a24]Advanced[/#f15a24]"
|
|
246
|
+
)
|
querri/cli/_context.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""CLI context helpers — construct SDK clients from CLI args and env vars."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from querri._auth import TokenProfile
|
|
13
|
+
|
|
14
|
+
from querri._client import Querri
|
|
15
|
+
from querri._exceptions import ConfigError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_client(ctx: typer.Context) -> Querri:
|
|
19
|
+
"""Construct a Querri client from CLI context options.
|
|
20
|
+
|
|
21
|
+
Resolution order:
|
|
22
|
+
1. Explicit ``--api-key`` flag (wins)
|
|
23
|
+
2. Environment variables (``QUERRI_API_KEY``, ``QUERRI_ACCESS_TOKEN``)
|
|
24
|
+
3. Token store (``~/.querri/tokens.json`` — active profile, auto-refresh)
|
|
25
|
+
4. Error with guidance
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Configured ``Querri`` client instance.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
typer.Exit: With code 2 on authentication/config errors.
|
|
32
|
+
"""
|
|
33
|
+
obj: dict[str, Any] = ctx.ensure_object(dict)
|
|
34
|
+
|
|
35
|
+
# 1. Explicit --api-key wins
|
|
36
|
+
api_key = obj.get("api_key")
|
|
37
|
+
org_id = obj.get("org_id")
|
|
38
|
+
host = obj.get("host")
|
|
39
|
+
|
|
40
|
+
if api_key:
|
|
41
|
+
try:
|
|
42
|
+
return Querri(api_key=api_key, org_id=org_id, host=host)
|
|
43
|
+
except ConfigError as exc:
|
|
44
|
+
_handle_config_error(obj, exc)
|
|
45
|
+
raise typer.Exit(code=2) from None
|
|
46
|
+
|
|
47
|
+
# 2. Environment variables — let resolve_config handle them
|
|
48
|
+
if os.environ.get("QUERRI_API_KEY") or os.environ.get("QUERRI_ACCESS_TOKEN"):
|
|
49
|
+
try:
|
|
50
|
+
return Querri(org_id=org_id, host=host)
|
|
51
|
+
except ConfigError as exc:
|
|
52
|
+
_handle_config_error(obj, exc)
|
|
53
|
+
raise typer.Exit(code=2) from None
|
|
54
|
+
|
|
55
|
+
# 3. Token store — load active profile, auto-refresh if needed
|
|
56
|
+
try:
|
|
57
|
+
from querri._auth import TokenStore, needs_refresh, refresh_tokens
|
|
58
|
+
from querri._config import DEFAULT_HOST
|
|
59
|
+
|
|
60
|
+
profile_name = obj.get("profile") or "default"
|
|
61
|
+
store = TokenStore.load()
|
|
62
|
+
profile = store.profiles.get(profile_name)
|
|
63
|
+
|
|
64
|
+
if profile and profile.access_token:
|
|
65
|
+
# Use the host stored in the profile (from login), CLI flag, env, or default
|
|
66
|
+
resolved_host = (
|
|
67
|
+
host or os.environ.get("QUERRI_HOST") or profile.host or DEFAULT_HOST
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Auto-refresh if near expiry
|
|
71
|
+
if needs_refresh(profile):
|
|
72
|
+
try:
|
|
73
|
+
profile = refresh_tokens(profile, resolved_host)
|
|
74
|
+
store.save_profile(profile_name, profile)
|
|
75
|
+
except Exception:
|
|
76
|
+
# Refresh failed (RuntimeError, timeout, network error, etc.)
|
|
77
|
+
# but the token may still be valid — needs_refresh fires
|
|
78
|
+
# early as a buffer. Fall through to use it anyway.
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return Querri(
|
|
82
|
+
access_token=profile.access_token,
|
|
83
|
+
org_id=profile.org_id or org_id,
|
|
84
|
+
host=resolved_host,
|
|
85
|
+
)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
# Token store error — print debug info in verbose mode and fall through
|
|
88
|
+
if obj.get("verbose"):
|
|
89
|
+
print(f"Token store error: {exc}", file=sys.stderr)
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
# 4. Nothing worked
|
|
93
|
+
_handle_config_error(
|
|
94
|
+
obj,
|
|
95
|
+
ConfigError(
|
|
96
|
+
"No credentials found. Run 'querri auth login' or set QUERRI_API_KEY."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
raise typer.Exit(code=2)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _handle_config_error(obj: dict[str, Any], exc: ConfigError) -> None:
|
|
103
|
+
"""Print a user-friendly config error message."""
|
|
104
|
+
is_json = obj.get("json", False)
|
|
105
|
+
|
|
106
|
+
if is_json:
|
|
107
|
+
import json
|
|
108
|
+
|
|
109
|
+
error_obj = {
|
|
110
|
+
"error": "auth_failed",
|
|
111
|
+
"message": str(exc),
|
|
112
|
+
"hint": "Run 'querri auth login' or set QUERRI_API_KEY and QUERRI_ORG_ID "
|
|
113
|
+
"environment variables.",
|
|
114
|
+
"code": 2,
|
|
115
|
+
}
|
|
116
|
+
print(json.dumps(error_obj))
|
|
117
|
+
else:
|
|
118
|
+
from querri.cli._output import print_error
|
|
119
|
+
|
|
120
|
+
print_error(str(exc))
|
|
121
|
+
print(
|
|
122
|
+
"\nTo get started:\n"
|
|
123
|
+
" querri auth login # Browser-based login\n"
|
|
124
|
+
" export QUERRI_API_KEY=qk_... # API key auth\n"
|
|
125
|
+
" export QUERRI_ORG_ID=org_...\n"
|
|
126
|
+
"\nOr pass them as flags:\n"
|
|
127
|
+
" querri --api-key qk_... --org-id org_... <command>",
|
|
128
|
+
file=sys.stderr,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Workspace state helpers
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_profile(ctx: typer.Context) -> TokenProfile | None:
|
|
138
|
+
"""Load the active TokenProfile (or None)."""
|
|
139
|
+
from querri._auth import TokenStore
|
|
140
|
+
|
|
141
|
+
obj = ctx.ensure_object(dict)
|
|
142
|
+
profile_name = obj.get("profile") or "default"
|
|
143
|
+
store = TokenStore.load()
|
|
144
|
+
return store.profiles.get(profile_name)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _save_profile(ctx: typer.Context, profile: "TokenProfile") -> None:
|
|
148
|
+
"""Persist updates to the active TokenProfile."""
|
|
149
|
+
from querri._auth import TokenStore
|
|
150
|
+
|
|
151
|
+
obj = ctx.ensure_object(dict)
|
|
152
|
+
profile_name = obj.get("profile") or "default"
|
|
153
|
+
store = TokenStore.load()
|
|
154
|
+
store.save_profile(profile_name, profile)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def resolve_project_id(ctx: typer.Context) -> str:
|
|
158
|
+
"""Resolve project ID from --project flag → stored state → error."""
|
|
159
|
+
obj = ctx.ensure_object(dict)
|
|
160
|
+
is_json = obj.get("json", False)
|
|
161
|
+
|
|
162
|
+
# 1. Explicit --project flag
|
|
163
|
+
project: str | None = obj.get("project")
|
|
164
|
+
if project:
|
|
165
|
+
return project
|
|
166
|
+
|
|
167
|
+
# 2. Stored active project
|
|
168
|
+
profile = _get_profile(ctx)
|
|
169
|
+
if profile and profile.active_project_id:
|
|
170
|
+
return str(profile.active_project_id)
|
|
171
|
+
|
|
172
|
+
# 3. Error
|
|
173
|
+
from querri.cli._output import print_error
|
|
174
|
+
|
|
175
|
+
if is_json:
|
|
176
|
+
from querri.cli._output import print_json_error
|
|
177
|
+
|
|
178
|
+
print_json_error(
|
|
179
|
+
"no_project",
|
|
180
|
+
"No active project. Run 'querri project select <name>' or pass --project.",
|
|
181
|
+
1,
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
print_error(
|
|
185
|
+
"No active project.\n"
|
|
186
|
+
" querri project select <name> # Set active project\n"
|
|
187
|
+
" querri project new # Create a new project\n"
|
|
188
|
+
" --project <id> # Pass explicitly"
|
|
189
|
+
)
|
|
190
|
+
raise typer.Exit(code=1)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def resolve_user_id(ctx: typer.Context) -> str:
|
|
194
|
+
"""Resolve user ID from env → stored profile → error."""
|
|
195
|
+
env_user = os.environ.get("QUERRI_USER_ID")
|
|
196
|
+
if env_user:
|
|
197
|
+
return env_user
|
|
198
|
+
|
|
199
|
+
profile = _get_profile(ctx)
|
|
200
|
+
if profile and profile.user_id:
|
|
201
|
+
return str(profile.user_id)
|
|
202
|
+
|
|
203
|
+
obj = ctx.ensure_object(dict)
|
|
204
|
+
from querri.cli._output import print_error
|
|
205
|
+
|
|
206
|
+
if obj.get("json"):
|
|
207
|
+
from querri.cli._output import print_json_error
|
|
208
|
+
|
|
209
|
+
print_json_error("no_user_id", "Could not determine user ID.", 1)
|
|
210
|
+
else:
|
|
211
|
+
print_error(
|
|
212
|
+
"Could not determine user ID. "
|
|
213
|
+
"Set QUERRI_USER_ID or log in with "
|
|
214
|
+
"'querri auth login'."
|
|
215
|
+
)
|
|
216
|
+
raise typer.Exit(code=1)
|