deepctl-cmd-debug-toolkit 0.1.0__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.
- deepctl_cmd_debug_toolkit/__init__.py +5 -0
- deepctl_cmd_debug_toolkit/command.py +143 -0
- deepctl_cmd_debug_toolkit/fetcher.py +145 -0
- deepctl_cmd_debug_toolkit/models.py +20 -0
- deepctl_cmd_debug_toolkit-0.1.0.dist-info/METADATA +66 -0
- deepctl_cmd_debug_toolkit-0.1.0.dist-info/RECORD +9 -0
- deepctl_cmd_debug_toolkit-0.1.0.dist-info/WHEEL +5 -0
- deepctl_cmd_debug_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- deepctl_cmd_debug_toolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Toolkit subcommand group — runs field support scripts from deepgram/support-toolkit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from deepctl_core import AuthManager, BaseGroupCommand, Config
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from .fetcher import get_cached_manifest, get_or_fetch_script, refresh_manifest
|
|
15
|
+
from .models import ToolkitScript
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ToolkitCommand(BaseGroupCommand):
|
|
21
|
+
"""Runs field engineering scripts from deepgram/support-toolkit."""
|
|
22
|
+
|
|
23
|
+
name = "toolkit"
|
|
24
|
+
help = (
|
|
25
|
+
"Run field engineering diagnostic scripts fetched from "
|
|
26
|
+
"deepgram/support-toolkit. Scripts are downloaded on first use, "
|
|
27
|
+
"verified against their GitHub blob SHA, and cached locally."
|
|
28
|
+
)
|
|
29
|
+
short_help = "Field support scripts (deepgram/support-toolkit)"
|
|
30
|
+
|
|
31
|
+
requires_auth = False
|
|
32
|
+
requires_project = False
|
|
33
|
+
ci_friendly = True
|
|
34
|
+
invoke_without_command = False
|
|
35
|
+
|
|
36
|
+
examples = [
|
|
37
|
+
"dg debug toolkit refresh",
|
|
38
|
+
"dg debug toolkit latency wss://api.deepgram.com/v1/listen",
|
|
39
|
+
"dg debug toolkit latency wss://api.deepgram.com/v1/listen --duration=60",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
def setup_commands(self) -> list[click.Command]:
|
|
43
|
+
"""Return commands derived from the cached manifest.
|
|
44
|
+
|
|
45
|
+
Intentionally never performs network I/O — reads disk cache only so
|
|
46
|
+
that every `dg` invocation stays fast. Users run `refresh` once to
|
|
47
|
+
populate the cache; individual script commands fetch lazily at invoke
|
|
48
|
+
time.
|
|
49
|
+
"""
|
|
50
|
+
commands: list[click.Command] = [_make_refresh_command()]
|
|
51
|
+
|
|
52
|
+
manifest = get_cached_manifest()
|
|
53
|
+
if manifest:
|
|
54
|
+
for name, entry in manifest.commands.items():
|
|
55
|
+
commands.append(_make_script_command(name, entry))
|
|
56
|
+
|
|
57
|
+
return commands
|
|
58
|
+
|
|
59
|
+
def handle_group(
|
|
60
|
+
self,
|
|
61
|
+
config: Config,
|
|
62
|
+
auth_manager: AuthManager,
|
|
63
|
+
client: Any,
|
|
64
|
+
**kwargs: Any,
|
|
65
|
+
) -> None:
|
|
66
|
+
# BaseGroupCommand.handle() calls handle_group() even when a subcommand
|
|
67
|
+
# is being invoked — guard here so we don't add noise to subcommand runs.
|
|
68
|
+
if click.get_current_context().invoked_subcommand is not None:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
manifest = get_cached_manifest()
|
|
72
|
+
if not manifest:
|
|
73
|
+
console.print(
|
|
74
|
+
"[yellow]No toolkit scripts cached yet.[/yellow]\n"
|
|
75
|
+
"Run [bold]dg debug toolkit refresh[/bold] to fetch the "
|
|
76
|
+
"latest scripts from deepgram/support-toolkit."
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
console.print("[bold]Available toolkit scripts:[/bold]\n")
|
|
81
|
+
for name, entry in manifest.commands.items():
|
|
82
|
+
console.print(f" [bold]dg debug toolkit {name}[/bold]")
|
|
83
|
+
console.print(f" [dim]{entry.description}[/dim]\n")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _make_refresh_command() -> click.Command:
|
|
87
|
+
"""Static 'refresh' command — always available regardless of cache state."""
|
|
88
|
+
|
|
89
|
+
@click.command(
|
|
90
|
+
"refresh", help="Fetch the latest script list from deepgram/support-toolkit."
|
|
91
|
+
)
|
|
92
|
+
def _refresh() -> None:
|
|
93
|
+
try:
|
|
94
|
+
manifest = refresh_manifest(console)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
console.print(f"[red]Error fetching manifest:[/red] {exc}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
console.print(
|
|
100
|
+
f"\n[green]✓[/green] Toolkit manifest updated "
|
|
101
|
+
f"({len(manifest.commands)} script(s) available):\n"
|
|
102
|
+
)
|
|
103
|
+
for name, entry in manifest.commands.items():
|
|
104
|
+
console.print(f" [bold]dg debug toolkit {name}[/bold]")
|
|
105
|
+
console.print(f" [dim]{entry.description}[/dim]\n")
|
|
106
|
+
|
|
107
|
+
return _refresh
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _make_script_command(name: str, entry: ToolkitScript) -> click.Command:
|
|
111
|
+
"""Create a Click command that fetches, verifies, and runs a toolkit script."""
|
|
112
|
+
|
|
113
|
+
@click.command(
|
|
114
|
+
name,
|
|
115
|
+
help=entry.description,
|
|
116
|
+
# Forward all unrecognised args directly to the underlying script.
|
|
117
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
118
|
+
)
|
|
119
|
+
@click.argument("script_args", nargs=-1, type=click.UNPROCESSED)
|
|
120
|
+
def _run(script_args: tuple[str, ...]) -> None:
|
|
121
|
+
try:
|
|
122
|
+
script_path = get_or_fetch_script(entry, console)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
console.print(f"[red]Error fetching script:[/red] {exc}")
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
env = os.environ.copy()
|
|
128
|
+
|
|
129
|
+
if entry.pass_api_key:
|
|
130
|
+
try:
|
|
131
|
+
api_key = AuthManager(Config()).get_api_key()
|
|
132
|
+
if api_key:
|
|
133
|
+
env["DEEPGRAM_API_KEY"] = api_key
|
|
134
|
+
except Exception:
|
|
135
|
+
pass # Script will surface its own missing-key error
|
|
136
|
+
|
|
137
|
+
result = subprocess.run(
|
|
138
|
+
[sys.executable, str(script_path)] + list(script_args),
|
|
139
|
+
env=env,
|
|
140
|
+
)
|
|
141
|
+
sys.exit(result.returncode)
|
|
142
|
+
|
|
143
|
+
return _run
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""GitHub fetch, blob-SHA integrity verification, and local cache management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import platformdirs
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from .models import ToolkitManifest, ToolkitScript
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
GITHUB_REPO = "deepgram/support-toolkit"
|
|
22
|
+
GITHUB_API_BASE = f"https://api.github.com/repos/{GITHUB_REPO}/contents"
|
|
23
|
+
|
|
24
|
+
_CACHE_DIR = Path(platformdirs.user_cache_dir("deepctl")) / "toolkit"
|
|
25
|
+
_MANIFEST_CACHE = _CACHE_DIR / "manifest.json"
|
|
26
|
+
_MANIFEST_AGE = _CACHE_DIR / "manifest.age"
|
|
27
|
+
_SCRIPTS_CACHE = _CACHE_DIR / "scripts"
|
|
28
|
+
_MANIFEST_TTL = 86400 # 24 hours
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def git_blob_sha(content: bytes) -> str:
|
|
32
|
+
"""Compute the git blob SHA1 for integrity verification.
|
|
33
|
+
|
|
34
|
+
GitHub's Contents API returns this hash alongside file content.
|
|
35
|
+
Re-deriving it from the downloaded bytes lets us confirm we received
|
|
36
|
+
exactly what GitHub has, using their own object model as a trust anchor.
|
|
37
|
+
"""
|
|
38
|
+
header = f"blob {len(content)}\0".encode()
|
|
39
|
+
return hashlib.sha1(header + content).hexdigest()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def local_toolkit_path() -> Path | None:
|
|
43
|
+
"""Return a local override path if DEEPGRAM_TOOLKIT_PATH is set."""
|
|
44
|
+
p = os.getenv("DEEPGRAM_TOOLKIT_PATH")
|
|
45
|
+
return Path(p) if p else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _fetch_github_contents(path: str) -> tuple[str, bytes]:
|
|
49
|
+
"""Fetch a file from the GitHub Contents API.
|
|
50
|
+
|
|
51
|
+
Returns (blob_sha, raw_bytes). Raises on any HTTP error or SHA mismatch.
|
|
52
|
+
"""
|
|
53
|
+
url = f"{GITHUB_API_BASE}/{path}"
|
|
54
|
+
resp = httpx.get(
|
|
55
|
+
url,
|
|
56
|
+
headers={"Accept": "application/vnd.github+json"},
|
|
57
|
+
timeout=30.0,
|
|
58
|
+
follow_redirects=True,
|
|
59
|
+
)
|
|
60
|
+
resp.raise_for_status()
|
|
61
|
+
data = resp.json()
|
|
62
|
+
sha: str = data["sha"]
|
|
63
|
+
content = base64.b64decode(data["content"])
|
|
64
|
+
|
|
65
|
+
# Verify the download is exactly what GitHub reported.
|
|
66
|
+
if git_blob_sha(content) != sha:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
f"Integrity check failed for {path}: "
|
|
69
|
+
f"downloaded content does not match GitHub blob SHA."
|
|
70
|
+
)
|
|
71
|
+
return sha, content
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_cached_manifest() -> ToolkitManifest | None:
|
|
75
|
+
"""Return the cached manifest if it exists and is fresh, else None."""
|
|
76
|
+
if not _MANIFEST_CACHE.exists():
|
|
77
|
+
return None
|
|
78
|
+
if _MANIFEST_AGE.exists():
|
|
79
|
+
try:
|
|
80
|
+
age = float(_MANIFEST_AGE.read_text().strip())
|
|
81
|
+
if time.time() - age > _MANIFEST_TTL:
|
|
82
|
+
return None # Stale — caller should refresh
|
|
83
|
+
except Exception:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return ToolkitManifest.model_validate_json(_MANIFEST_CACHE.read_text())
|
|
87
|
+
except Exception:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def refresh_manifest(console: Console) -> ToolkitManifest:
|
|
92
|
+
"""Fetch the toolkit manifest, verify it, cache it, and return it."""
|
|
93
|
+
local = local_toolkit_path()
|
|
94
|
+
if local:
|
|
95
|
+
manifest_file = local / "toolkit.json"
|
|
96
|
+
content = manifest_file.read_bytes()
|
|
97
|
+
console.print(f"[dim]Loading toolkit manifest from {local}[/dim]")
|
|
98
|
+
else:
|
|
99
|
+
console.print(
|
|
100
|
+
f"[dim]Fetching toolkit manifest from github.com/{GITHUB_REPO}...[/dim]"
|
|
101
|
+
)
|
|
102
|
+
_sha, content = _fetch_github_contents("toolkit.json")
|
|
103
|
+
|
|
104
|
+
manifest = ToolkitManifest.model_validate_json(content)
|
|
105
|
+
|
|
106
|
+
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
_MANIFEST_CACHE.write_bytes(content)
|
|
108
|
+
_MANIFEST_AGE.write_text(str(time.time()))
|
|
109
|
+
|
|
110
|
+
return manifest
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_or_fetch_script(entry: ToolkitScript, console: Console) -> Path:
|
|
114
|
+
"""Return the path to a verified local copy of the script.
|
|
115
|
+
|
|
116
|
+
Uses a local override if DEEPGRAM_TOOLKIT_PATH is set, otherwise
|
|
117
|
+
fetches from GitHub and caches by blob SHA. Re-downloads only when the
|
|
118
|
+
cached SHA no longer matches the stored value.
|
|
119
|
+
"""
|
|
120
|
+
local = local_toolkit_path()
|
|
121
|
+
if local:
|
|
122
|
+
return local / entry.script
|
|
123
|
+
|
|
124
|
+
# Stable cache filename derived from the script path.
|
|
125
|
+
safe_name = entry.script.replace("/", "__")
|
|
126
|
+
cache_file = _SCRIPTS_CACHE / safe_name
|
|
127
|
+
sha_file = _SCRIPTS_CACHE / f"{safe_name}.sha"
|
|
128
|
+
|
|
129
|
+
# Use cache if content still matches its stored SHA.
|
|
130
|
+
if cache_file.exists() and sha_file.exists():
|
|
131
|
+
stored_sha = sha_file.read_text().strip()
|
|
132
|
+
if git_blob_sha(cache_file.read_bytes()) == stored_sha:
|
|
133
|
+
return cache_file
|
|
134
|
+
|
|
135
|
+
# Download from GitHub with integrity verification.
|
|
136
|
+
script_name = Path(entry.script).name
|
|
137
|
+
console.print(f"[blue]Downloading[/blue] {script_name} from support-toolkit...")
|
|
138
|
+
sha, content = _fetch_github_contents(entry.script)
|
|
139
|
+
|
|
140
|
+
_SCRIPTS_CACHE.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
cache_file.write_bytes(content)
|
|
142
|
+
sha_file.write_text(sha)
|
|
143
|
+
|
|
144
|
+
console.print(f"[green]✓[/green] {script_name} verified (sha: {sha[:12]}…)")
|
|
145
|
+
return cache_file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Models for the toolkit manifest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolkitScript(BaseModel):
|
|
9
|
+
"""A single script entry from toolkit.json."""
|
|
10
|
+
|
|
11
|
+
description: str
|
|
12
|
+
script: str # Relative path within the support-toolkit repo
|
|
13
|
+
pass_api_key: bool = False # Inject DEEPGRAM_API_KEY into subprocess env
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ToolkitManifest(BaseModel):
|
|
17
|
+
"""Parsed representation of toolkit.json."""
|
|
18
|
+
|
|
19
|
+
version: str
|
|
20
|
+
commands: dict[str, ToolkitScript]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepctl-cmd-debug-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Toolkit subcommand for dg debug — runs field support scripts from deepgram/support-toolkit
|
|
5
|
+
Author-email: Deepgram <devrel@deepgram.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: deepctl-core>=0.1.10
|
|
10
|
+
Requires-Dist: httpx>=0.27.0
|
|
11
|
+
Requires-Dist: pydantic>=2.10.1
|
|
12
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
13
|
+
Requires-Dist: rich>=13.9.4
|
|
14
|
+
Requires-Dist: click>=8.1.7
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.3.3; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
|
|
18
|
+
Requires-Dist: mypy>=1.13.0; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.8.1; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# deepctl-cmd-debug-toolkit
|
|
22
|
+
|
|
23
|
+
> Part of [deepctl](https://github.com/deepgram/cli) — Official Deepgram CLI
|
|
24
|
+
|
|
25
|
+
Toolkit subcommand for dg debug — runs field support scripts from deepgram/support-toolkit
|
|
26
|
+
|
|
27
|
+
This is a subcommand of `deepctl debug`.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
This package is included with deepctl and does not need to be installed separately.
|
|
32
|
+
|
|
33
|
+
### Install deepctl
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Install with pip
|
|
37
|
+
pip install deepctl
|
|
38
|
+
|
|
39
|
+
# Or install with uv
|
|
40
|
+
uv tool install deepctl
|
|
41
|
+
|
|
42
|
+
# Or install with pipx
|
|
43
|
+
pipx install deepctl
|
|
44
|
+
|
|
45
|
+
# Or run without installing
|
|
46
|
+
uvx deepctl --help
|
|
47
|
+
pipx run deepctl --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
| Command | Entry Point |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `deepctl debug toolkit` | `deepctl_cmd_debug_toolkit.command:ToolkitCommand` |
|
|
55
|
+
|
|
56
|
+
## Dependencies
|
|
57
|
+
|
|
58
|
+
- `httpx>=0.27.0`
|
|
59
|
+
- `pydantic>=2.10.1`
|
|
60
|
+
- `platformdirs>=4.0.0`
|
|
61
|
+
- `rich>=13.9.4`
|
|
62
|
+
- `click>=8.1.7`
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT — see [LICENSE](../../LICENSE)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
deepctl_cmd_debug_toolkit/__init__.py,sha256=koXSDCTIRP3qm6Aa__0bGkrfMGP0QQK53EEwk5VbKcI,162
|
|
2
|
+
deepctl_cmd_debug_toolkit/command.py,sha256=kXK386bSHtWcHsn_c8C3Lwql5ycSGC8VRttAEemXHgw,4909
|
|
3
|
+
deepctl_cmd_debug_toolkit/fetcher.py,sha256=xeeefB8Ev672k0VCEEVFC_89soP_1gk5jvnY1Lu3AOQ,4832
|
|
4
|
+
deepctl_cmd_debug_toolkit/models.py,sha256=boToXfTJWbRXmZdbmwhSByul3V8TySUGTaSlp-IkAjI,499
|
|
5
|
+
deepctl_cmd_debug_toolkit-0.1.0.dist-info/METADATA,sha256=KUbeEu83nRjHWM-wBo3ZlngNRnPJeTrOT51O5lI-84Y,1590
|
|
6
|
+
deepctl_cmd_debug_toolkit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
deepctl_cmd_debug_toolkit-0.1.0.dist-info/entry_points.txt,sha256=L1Lk43utOWOLoEMRdwWC7OX0re75omA4y9ZIwAvJyw8,87
|
|
8
|
+
deepctl_cmd_debug_toolkit-0.1.0.dist-info/top_level.txt,sha256=nY0EOsmsW2gdj-lQZNzh8bhkrGXZMvnNpLLy6FJJpfQ,26
|
|
9
|
+
deepctl_cmd_debug_toolkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deepctl_cmd_debug_toolkit
|