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.
Files changed (22) hide show
  1. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/PKG-INFO +1 -1
  2. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/pyproject.toml +1 -1
  3. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/__init__.py +2 -0
  4. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/auth.py +5 -2
  5. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/base_command.py +54 -12
  6. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/base_group_command.py +5 -2
  7. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/client.py +6 -2
  8. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/output.py +24 -0
  9. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/plugin_manager.py +78 -3
  10. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/timing.py +5 -2
  11. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/PKG-INFO +1 -1
  12. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/README.md +0 -0
  13. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/setup.cfg +0 -0
  14. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/config.py +0 -0
  15. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/models.py +0 -0
  16. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/plugin_env.py +0 -0
  17. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/py.typed +0 -0
  18. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core/skill_generator.py +0 -0
  19. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/SOURCES.txt +0 -0
  20. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/dependency_links.txt +0 -0
  21. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/requires.txt +0 -0
  22. {deepctl_core-0.2.16 → deepctl_core-0.2.18}/src/deepctl_core.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-core
3
- Version: 0.2.16
3
+ Version: 0.2.18
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepctl-core"
7
- version = "0.2.16" # x-release-please-version
7
+ version = "0.2.18" # x-release-please-version
8
8
  description = "Core components for deepctl"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- console = Console()
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 .output import _agentic, print_error, print_info, print_warning, stderr_console
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 = 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
- print_info(f"Using credentials from {source}")
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
- print_info(f"Affecting project: {project_id}")
120
+ stderr_console.print(
121
+ f"{info} Affecting project: {project_id}"
122
+ )
105
123
  else:
106
- print_warning("No project ID specified")
107
-
108
- except Exception:
109
- # guard() already printed helpful error messages;
110
- # exit without duplicating them.
111
- raise SystemExit(1)
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 (BrokenPipeError, OSError):
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
- console = Console()
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
- from rich.console import Console
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
- console = Console()
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
- console = Console()
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 rich.console import Console
10
+ from .output import stderr_console
11
11
 
12
- console = Console()
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-core
3
- Version: 0.2.16
3
+ Version: 0.2.18
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
File without changes
File without changes