deepctl-core 0.2.16__tar.gz → 0.2.18__tar.gz
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.
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/PKG-INFO +1 -1
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/pyproject.toml +1 -1
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/__init__.py +2 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/auth.py +5 -2
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/base_command.py +54 -12
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/base_group_command.py +5 -2
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/client.py +6 -2
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/output.py +24 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/plugin_manager.py +78 -3
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/timing.py +5 -2
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/PKG-INFO +1 -1
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/README.md +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/setup.cfg +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/config.py +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/models.py +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/plugin_env.py +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/py.typed +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/skill_generator.py +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/SOURCES.txt +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/dependency_links.txt +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/requires.txt +0 -0
- {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/top_level.txt +0 -0
|
@@ -24,6 +24,7 @@ from .output import (
|
|
|
24
24
|
print_success,
|
|
25
25
|
print_warning,
|
|
26
26
|
setup_output,
|
|
27
|
+
update_output,
|
|
27
28
|
)
|
|
28
29
|
from .plugin_manager import PluginManager
|
|
29
30
|
from .timing import (
|
|
@@ -63,4 +64,5 @@ __all__ = [
|
|
|
63
64
|
"print_timing_summary",
|
|
64
65
|
"print_warning",
|
|
65
66
|
"setup_output",
|
|
67
|
+
"update_output",
|
|
66
68
|
]
|
|
@@ -9,14 +9,17 @@ from urllib.parse import urlencode
|
|
|
9
9
|
import httpx
|
|
10
10
|
import keyring
|
|
11
11
|
from pydantic import BaseModel
|
|
12
|
-
from rich.console import Console
|
|
13
12
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
14
13
|
|
|
15
14
|
from .client import _split_base_url
|
|
16
15
|
from .config import Config
|
|
17
16
|
from .models import ProfileInfo, ProfilesResult
|
|
17
|
+
from .output import stderr_console
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Diagnostics only: everything printed here is status or error chrome, so
|
|
20
|
+
# it goes to stderr and can never corrupt the machine-readable payload a
|
|
21
|
+
# command writes to stdout. See get_status_console() in output.py.
|
|
22
|
+
console = stderr_console
|
|
20
23
|
|
|
21
24
|
# Auth provider base URL (dx-id OIDC provider)
|
|
22
25
|
AUTH_BASE_URL = os.getenv("DEEPGRAM_CLI_BASE_URL", "https://id.dx.deepgram.com")
|
|
@@ -4,15 +4,20 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from typing import Any, ClassVar
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
|
-
from rich.console import Console
|
|
8
7
|
|
|
9
8
|
from .auth import AuthManager
|
|
10
9
|
from .client import DeepgramClient
|
|
11
10
|
from .config import Config
|
|
12
|
-
from .
|
|
11
|
+
from .models import ErrorResult
|
|
12
|
+
from .output import _agentic, print_error, stderr_console
|
|
13
|
+
from .output import console as stdout_console
|
|
13
14
|
from .timing import TimingContext
|
|
14
15
|
|
|
15
|
-
console
|
|
16
|
+
# The PAYLOAD console -- stdout, deliberately. Tables, JSON and raw text
|
|
17
|
+
# written by _output_* are the machine-readable result and belong there.
|
|
18
|
+
# Shared instance rather than a bare Console() so it honours the agentic
|
|
19
|
+
# no-color/highlight settings; diagnostics use stderr_console instead.
|
|
20
|
+
console = stdout_console
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
class BaseCommand(ABC):
|
|
@@ -99,16 +104,52 @@ class BaseCommand(ABC):
|
|
|
99
104
|
"explicit flags",
|
|
100
105
|
"environment variables",
|
|
101
106
|
]:
|
|
102
|
-
|
|
107
|
+
# Diagnostics, not the result. print_info /
|
|
108
|
+
# print_warning still write to stdout outside
|
|
109
|
+
# agentic mode, so with an explicit `-o json`
|
|
110
|
+
# these three lines landed in front of the
|
|
111
|
+
# payload and broke `| jq` (#104, success
|
|
112
|
+
# path). Route them straight to stderr; the
|
|
113
|
+
# prefixes mirror output.py so the rendering
|
|
114
|
+
# is unchanged in both modes.
|
|
115
|
+
info = "INFO:" if _agentic else "[blue]ℹ[/blue]"
|
|
116
|
+
stderr_console.print(
|
|
117
|
+
f"{info} Using credentials from {source}"
|
|
118
|
+
)
|
|
103
119
|
if project_id:
|
|
104
|
-
|
|
120
|
+
stderr_console.print(
|
|
121
|
+
f"{info} Affecting project: {project_id}"
|
|
122
|
+
)
|
|
105
123
|
else:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
warn = "WARN:" if _agentic else "[yellow]⚠[/yellow]"
|
|
125
|
+
stderr_console.print(
|
|
126
|
+
f"{warn} No project ID specified"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
except Exception as auth_error:
|
|
130
|
+
# guard() already wrote the human-readable diagnosis to
|
|
131
|
+
# stderr, so don't duplicate it -- but stdout must still
|
|
132
|
+
# carry a parseable payload in a machine-readable
|
|
133
|
+
# format. A CI step doing `dg -o json ... > out.json`
|
|
134
|
+
# and parsing the result should get a structured error,
|
|
135
|
+
# not an empty file. output_result is a no-op in
|
|
136
|
+
# default mode, so this adds nothing for humans.
|
|
137
|
+
self._tag_telemetry_status("error")
|
|
138
|
+
try:
|
|
139
|
+
self.output_result(
|
|
140
|
+
ErrorResult(error=str(auth_error)), config
|
|
141
|
+
)
|
|
142
|
+
except OSError:
|
|
143
|
+
# Downstream stream closed. BrokenPipeError is an
|
|
144
|
+
# OSError, so one clause covers both.
|
|
145
|
+
pass
|
|
146
|
+
except ValueError as exc:
|
|
147
|
+
# Rich raises ValueError("I/O operation on closed
|
|
148
|
+
# file") when the stream went away mid-write; any
|
|
149
|
+
# other ValueError is a real bug.
|
|
150
|
+
if "closed file" not in str(exc):
|
|
151
|
+
raise
|
|
152
|
+
raise SystemExit(1) from auth_error
|
|
112
153
|
|
|
113
154
|
# Check project ID if required
|
|
114
155
|
if self.requires_project:
|
|
@@ -139,10 +180,11 @@ class BaseCommand(ABC):
|
|
|
139
180
|
with TimingContext("output_processing"):
|
|
140
181
|
try:
|
|
141
182
|
self.output_result(result, config)
|
|
142
|
-
except
|
|
183
|
+
except OSError:
|
|
143
184
|
# Downstream stream closed (e.g. an MCP host disconnected
|
|
144
185
|
# stdio after `dg mcp` finished). Nothing useful to log
|
|
145
186
|
# here because the logger writes to the same closed stream.
|
|
187
|
+
# BrokenPipeError is an OSError, so one clause covers both.
|
|
146
188
|
pass
|
|
147
189
|
except ValueError as exc:
|
|
148
190
|
if "closed file" not in str(exc):
|
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
|
-
from rich.console import Console
|
|
7
6
|
|
|
8
7
|
from .auth import AuthManager
|
|
9
8
|
from .base_command import BaseCommand
|
|
10
9
|
from .client import DeepgramClient
|
|
11
10
|
from .config import Config
|
|
11
|
+
from .output import stderr_console
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# Diagnostics only: everything printed here is status or error chrome, so
|
|
14
|
+
# it goes to stderr and can never corrupt the machine-readable payload a
|
|
15
|
+
# command writes to stdout. See get_status_console() in output.py.
|
|
16
|
+
console = stderr_console
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class BaseGroupCommand(BaseCommand):
|
|
@@ -9,7 +9,8 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
9
9
|
from deepgram import DeepgramClient as DGClient
|
|
10
10
|
from deepgram import DeepgramClientEnvironment
|
|
11
11
|
from deepgram.core.api_error import ApiError
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
from .output import stderr_console
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from collections.abc import Iterator
|
|
@@ -18,7 +19,10 @@ if TYPE_CHECKING:
|
|
|
18
19
|
from .auth import AuthManager
|
|
19
20
|
from .config import Config
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
# Diagnostics only: everything printed here is status or error chrome, so
|
|
23
|
+
# it goes to stderr and can never corrupt the machine-readable payload a
|
|
24
|
+
# command writes to stdout. See get_status_console() in output.py.
|
|
25
|
+
console = stderr_console
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def _split_base_url(base_url: str) -> tuple[str, str, str]:
|
|
@@ -103,6 +103,30 @@ def setup_output(
|
|
|
103
103
|
console.quiet = quiet
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def update_output(
|
|
107
|
+
format_type: str | None = None,
|
|
108
|
+
quiet: bool | None = None,
|
|
109
|
+
verbose: bool | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Update parts of the global output configuration in place.
|
|
112
|
+
|
|
113
|
+
Unlike setup_output(), only the arguments actually provided change;
|
|
114
|
+
every other setting keeps its current value. The per-command
|
|
115
|
+
pass-through options use this so that ``dg models -o json`` has the
|
|
116
|
+
same effect as ``dg -o json models`` without resetting the quiet or
|
|
117
|
+
verbose state the root group already applied.
|
|
118
|
+
"""
|
|
119
|
+
if format_type is not None:
|
|
120
|
+
if format_type == "default" and _output_config["agentic"]:
|
|
121
|
+
format_type = "json"
|
|
122
|
+
_output_config["format"] = format_type
|
|
123
|
+
if quiet is not None:
|
|
124
|
+
_output_config["quiet"] = quiet
|
|
125
|
+
console.quiet = quiet
|
|
126
|
+
if verbose is not None:
|
|
127
|
+
_output_config["verbose"] = verbose
|
|
128
|
+
|
|
129
|
+
|
|
106
130
|
class OutputFormatter:
|
|
107
131
|
"""Formatter for different output types."""
|
|
108
132
|
|
|
@@ -7,12 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
import click
|
|
10
|
-
from rich.console import Console
|
|
11
10
|
|
|
12
11
|
from .base_command import BaseCommand
|
|
13
12
|
from .base_group_command import BaseGroupCommand
|
|
14
13
|
from .models import ErrorResult, PluginInfo
|
|
15
|
-
from .output import print_warning
|
|
14
|
+
from .output import print_warning, stderr_console
|
|
16
15
|
from .plugin_env import (
|
|
17
16
|
PLUGIN_VENV,
|
|
18
17
|
get_plugin_state,
|
|
@@ -21,7 +20,9 @@ from .plugin_env import (
|
|
|
21
20
|
)
|
|
22
21
|
from .timing import TimingContext
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
# Load errors are diagnostics: they go to stderr so a broken plugin can't
|
|
24
|
+
# corrupt `dg ... -o json` payloads on stdout.
|
|
25
|
+
console = stderr_console
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class PluginManager:
|
|
@@ -293,6 +294,80 @@ class PluginManager:
|
|
|
293
294
|
help="Skip interactive prompts; use defaults for any optional features.",
|
|
294
295
|
)(cmd)
|
|
295
296
|
|
|
297
|
+
# Accept the global output options after the subcommand as well.
|
|
298
|
+
cmd = self._add_global_passthrough_options(cmd)
|
|
299
|
+
|
|
300
|
+
return cmd
|
|
301
|
+
|
|
302
|
+
def _add_global_passthrough_options(self, cmd: click.Command) -> click.Command:
|
|
303
|
+
"""Let the global output options work after the subcommand too.
|
|
304
|
+
|
|
305
|
+
The root group defines -o/--output, -q/--quiet and -v/--verbose, but
|
|
306
|
+
Click only parses group options that appear *before* the subcommand
|
|
307
|
+
name — so ``dg models -o json`` used to fail with "No such option
|
|
308
|
+
'-o'" while ``dg -o json models`` worked, and the error gave no hint.
|
|
309
|
+
These pass-through copies apply the same effect from the subcommand
|
|
310
|
+
position. A copy is only added when the command does not already
|
|
311
|
+
define the option name itself (for example, ``dg speak --output``
|
|
312
|
+
names an audio file and keeps its own meaning).
|
|
313
|
+
"""
|
|
314
|
+
from .output import update_output
|
|
315
|
+
|
|
316
|
+
taken: set[str] = set()
|
|
317
|
+
for param in cmd.params:
|
|
318
|
+
taken.update(param.opts)
|
|
319
|
+
taken.update(param.secondary_opts)
|
|
320
|
+
|
|
321
|
+
def _apply_format(
|
|
322
|
+
_ctx: click.Context, _param: click.Parameter, value: str | None
|
|
323
|
+
) -> None:
|
|
324
|
+
if value is not None:
|
|
325
|
+
update_output(format_type=value)
|
|
326
|
+
|
|
327
|
+
def _apply_quiet(
|
|
328
|
+
_ctx: click.Context, _param: click.Parameter, value: bool
|
|
329
|
+
) -> None:
|
|
330
|
+
if value:
|
|
331
|
+
update_output(quiet=True)
|
|
332
|
+
|
|
333
|
+
def _apply_verbose(
|
|
334
|
+
_ctx: click.Context, _param: click.Parameter, value: bool
|
|
335
|
+
) -> None:
|
|
336
|
+
if value:
|
|
337
|
+
update_output(verbose=True)
|
|
338
|
+
|
|
339
|
+
if not {"--output", "-o"} & taken:
|
|
340
|
+
cmd = click.option(
|
|
341
|
+
"--output",
|
|
342
|
+
"-o",
|
|
343
|
+
type=click.Choice(
|
|
344
|
+
["json", "yaml", "table", "csv"], case_sensitive=False
|
|
345
|
+
),
|
|
346
|
+
expose_value=False,
|
|
347
|
+
callback=_apply_format,
|
|
348
|
+
help="Output format (same as the global -o before the command).",
|
|
349
|
+
)(cmd)
|
|
350
|
+
|
|
351
|
+
if not {"--quiet", "-q"} & taken:
|
|
352
|
+
cmd = click.option(
|
|
353
|
+
"--quiet",
|
|
354
|
+
"-q",
|
|
355
|
+
is_flag=True,
|
|
356
|
+
expose_value=False,
|
|
357
|
+
callback=_apply_quiet,
|
|
358
|
+
help="Suppress non-essential output.",
|
|
359
|
+
)(cmd)
|
|
360
|
+
|
|
361
|
+
if not {"--verbose", "-v"} & taken:
|
|
362
|
+
cmd = click.option(
|
|
363
|
+
"--verbose",
|
|
364
|
+
"-v",
|
|
365
|
+
is_flag=True,
|
|
366
|
+
expose_value=False,
|
|
367
|
+
callback=_apply_verbose,
|
|
368
|
+
help="Enable verbose output.",
|
|
369
|
+
)(cmd)
|
|
370
|
+
|
|
296
371
|
return cmd
|
|
297
372
|
|
|
298
373
|
def _build_help_text(self, instance: Any) -> str:
|
|
@@ -7,9 +7,12 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
from threading import local
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from
|
|
10
|
+
from .output import stderr_console
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# Diagnostics only: everything printed here is status or error chrome, so
|
|
13
|
+
# it goes to stderr and can never corrupt the machine-readable payload a
|
|
14
|
+
# command writes to stdout. See get_status_console() in output.py.
|
|
15
|
+
console = stderr_console
|
|
13
16
|
|
|
14
17
|
# Thread-local storage for timing data
|
|
15
18
|
_timing_data = local()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|