-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Expand file tree
/
Copy pathhelpers.py
More file actions
372 lines (288 loc) · 11.3 KB
/
helpers.py
File metadata and controls
372 lines (288 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
"""CLI helper utilities.
Provides common functionality for all CLI commands:
- Compatibility re-exports for runtime/auth helpers
- Error handling
- JSON/Rich output formatting
- Context management (current notebook/conversation)
This module is also the backward-compatible facade for older imports and test
patch targets; see ``cli.runtime``, ``cli.auth_runtime``, ``cli.context``, and
``cli.rendering`` for canonical helpers.
"""
import logging
from collections.abc import Awaitable, Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any, NoReturn, TypeVar
import click
from .. import auth as auth_helpers
from ..auth import AuthTokens
from ..paths import get_context_path
from ..types import ArtifactType
from . import auth_runtime as auth_runtime_helpers
from . import context as context_helpers
from . import input as input_helpers
from . import rendering as rendering_helpers
from . import research_import as research_import_helpers
from . import runtime as runtime_helpers
from ._encoding import safe_echo
from .error_handler import exit_with_code
from .resolve import (
_resolve_partial_id as _resolve_partial_id,
)
from .resolve import (
require_notebook as require_notebook,
)
from .resolve import (
resolve_artifact_id as resolve_artifact_id,
)
from .resolve import (
resolve_note_id as resolve_note_id,
)
from .resolve import (
resolve_notebook_id as resolve_notebook_id,
)
from .resolve import (
resolve_source_id as resolve_source_id,
)
from .resolve import (
resolve_source_ids as resolve_source_ids,
)
from .resolve import (
validate_id as validate_id,
)
if TYPE_CHECKING:
from ..types import Artifact
console = rendering_helpers.console
stderr_console = rendering_helpers.stderr_console
logger = logging.getLogger(__name__)
T = TypeVar("T")
ResearchImportResult = research_import_helpers.ResearchImportResult
def build_cookie_jar(*args: Any, **kwargs: Any) -> Any:
"""Compatibility patch target for auth cookie-jar construction."""
return auth_helpers.build_cookie_jar(*args, **kwargs)
def load_auth_from_storage(*args: Any, **kwargs: Any) -> Any:
"""Compatibility patch target for auth storage loading."""
return auth_helpers.load_auth_from_storage(*args, **kwargs)
def emit_status(
msg: str,
*,
json_output: bool,
style: str | None = None,
quiet: bool = False,
) -> None:
"""Emit a status / diagnostic line.
The ``quiet`` kwarg is forwarded to the renderer; when True, the line is
suppressed entirely. The renderer also honors the active root ``--quiet``
flag automatically inside a Click invocation.
"""
rendering_helpers.emit_status(
msg,
json_output=json_output,
style=style,
quiet=quiet,
stdout_console=console,
stderr_output_console=stderr_console,
)
def cli_name_to_artifact_type(name: str) -> ArtifactType | None:
"""Convert CLI artifact type name to ArtifactType enum."""
return rendering_helpers.cli_name_to_artifact_type(name)
# =============================================================================
# ASYNC EXECUTION
# =============================================================================
def run_async(coro):
"""Run async coroutine in sync context."""
return runtime_helpers.run_async(coro)
async def import_with_retry(
client,
notebook_id: str,
task_id: str,
sources: list[dict],
*,
max_elapsed: float = 1800,
initial_delay: float = 5,
backoff_factor: float = 2,
max_delay: float = 60,
json_output: bool = False,
) -> list[dict[str, str]]:
"""Compatibility wrapper for :func:`cli.research_import.import_with_retry`."""
return await research_import_helpers.import_with_retry(
client,
notebook_id,
task_id,
sources,
max_elapsed=max_elapsed,
initial_delay=initial_delay,
backoff_factor=backoff_factor,
max_delay=max_delay,
json_output=json_output,
output_console=console,
)
def _display_cited_import_selection(
cited_selection: research_import_helpers.CitedSourceSelection | None,
) -> None:
"""Compatibility wrapper for the research import cited-source display."""
research_import_helpers._display_cited_import_selection(
cited_selection,
output_console=console,
)
async def import_research_sources(
client,
notebook_id: str,
task_id: str,
sources: list[dict],
*,
report: str = "",
cited_only: bool = False,
max_elapsed: float = 1800,
json_output: bool = False,
status_message: str | None = None,
) -> research_import_helpers.ResearchImportResult:
"""Compatibility wrapper for :func:`cli.research_import.import_research_sources`."""
return await research_import_helpers.import_research_sources(
client,
notebook_id,
task_id,
sources,
report=report,
cited_only=cited_only,
max_elapsed=max_elapsed,
json_output=json_output,
status_message=status_message,
import_func=import_with_retry,
output_console=console,
)
# =============================================================================
# AUTHENTICATION
# =============================================================================
def get_client(ctx) -> tuple[dict, str, str]:
"""Get auth components from context."""
return auth_runtime_helpers.get_client(ctx)
def get_auth_tokens(ctx) -> AuthTokens:
"""Get AuthTokens object from context."""
return auth_runtime_helpers.get_auth_tokens(ctx)
# =============================================================================
# CONTEXT MANAGEMENT
# =============================================================================
def _current_storage_override() -> Path | None:
"""Resolve the active ``--storage`` override from the current Click context."""
return context_helpers._current_storage_override()
def _get_context_value(key: str) -> str | None:
"""Read a single value from context.json."""
return context_helpers._get_context_value(key, context_path_fn=get_context_path)
def _set_context_value(key: str, value: str | None) -> None:
"""Set or clear a single value in context.json."""
context_helpers._set_context_value(key, value, context_path_fn=get_context_path)
def get_current_notebook() -> str | None:
"""Get the current notebook ID from context."""
return context_helpers.get_current_notebook(context_path_fn=get_context_path)
def set_current_notebook(
notebook_id: str,
title: str | None = None,
is_owner: bool | None = None,
created_at: str | None = None,
):
"""Set the current notebook context."""
context_helpers.set_current_notebook(
notebook_id,
title=title,
is_owner=is_owner,
created_at=created_at,
context_path_fn=get_context_path,
)
def clear_context(*, clear_account: bool = False) -> bool:
"""Clear the current context.
By default, only notebook/conversation fields are cleared; account
metadata used for multi-account auth routing is preserved. ``auth logout``
passes ``clear_account=True`` to remove the whole file.
Returns True if a context file was changed or removed, False if none
existed or no clearable fields were present.
"""
return context_helpers.clear_context(
clear_account=clear_account, context_path_fn=get_context_path
)
def get_current_conversation() -> str | None:
"""Get the current conversation ID from context."""
return context_helpers.get_current_conversation(context_path_fn=get_context_path)
def set_current_conversation(conversation_id: str | None):
"""Set or clear the current conversation ID in context."""
context_helpers.set_current_conversation(conversation_id, context_path_fn=get_context_path)
def read_stdin_text(*, source_label: str = "stdin") -> str:
"""Read all of stdin as UTF-8 text and strip surrounding whitespace."""
return input_helpers.read_stdin_text(source_label=source_label)
def resolve_prompt(
argument_value: str | None,
prompt_file: str | None,
param_name: str = "prompt",
*,
required: bool = False,
) -> str:
"""Resolve prompt text from a positional argument or ``--prompt-file``."""
return input_helpers.resolve_prompt(
argument_value,
prompt_file,
param_name,
required=required,
)
# =============================================================================
# ERROR HANDLING
# =============================================================================
def handle_error(e: Exception):
"""Handle and display errors consistently."""
message = f"Error: {e}"
try:
console.print(f"[red]{message}[/red]")
except UnicodeEncodeError:
safe_echo(message, err=True)
exit_with_code(1)
def handle_auth_error(json_output: bool = False) -> NoReturn:
"""Handle authentication errors with helpful context."""
auth_runtime_helpers.handle_auth_error(json_output)
# =============================================================================
# DECORATORS
# =============================================================================
def with_auth_and_errors(
ctx: click.Context,
*,
command_name: str,
json_output: bool,
body: Callable[[AuthTokens], Awaitable[T]],
auth_loader: Callable[[click.Context], AuthTokens] | None = None,
) -> T:
"""Run a CLI command body with shared auth bootstrap and error handling."""
return auth_runtime_helpers.with_auth_and_errors(
ctx,
command_name=command_name,
json_output=json_output,
body=body,
auth_loader=auth_loader,
)
def with_client(f):
"""Decorator that handles auth, async execution, and errors for CLI commands."""
return auth_runtime_helpers.with_client(f)
# =============================================================================
# OUTPUT FORMATTING
# =============================================================================
def json_output_response(data: dict | list) -> None:
"""Print JSON response (no colors for machine parsing)."""
rendering_helpers.json_output_response(data)
def json_error_response(code: str, message: str, extra: dict | None = None) -> NoReturn:
"""Print JSON error and exit (no colors for machine parsing)."""
rendering_helpers.json_error_response(code, message, extra)
def display_research_sources(sources: list[dict], max_display: int = 10) -> None:
"""Display research sources in a formatted table."""
rendering_helpers._display_research_sources(
sources, max_display=max_display, output_console=console
)
def display_report(report: str, max_chars: int = 1000, json_hint: bool = True) -> None:
"""Display a research report, truncated for terminal output."""
rendering_helpers._display_report(
report, max_chars=max_chars, json_hint=json_hint, output_console=console
)
# =============================================================================
# TYPE DISPLAY HELPERS
# =============================================================================
def get_artifact_type_display(artifact: "Artifact") -> str:
"""Get display string for artifact type."""
return rendering_helpers.get_artifact_type_display(artifact)
def get_source_type_display(source_type: str) -> str:
"""Get display string for source type."""
return rendering_helpers.get_source_type_display(source_type)