deepctl-cmd-api 0.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.
- deepctl_cmd_api/__init__.py +5 -0
- deepctl_cmd_api/command.py +330 -0
- deepctl_cmd_api/models.py +15 -0
- deepctl_cmd_api-0.0.2.dist-info/METADATA +65 -0
- deepctl_cmd_api-0.0.2.dist-info/RECORD +8 -0
- deepctl_cmd_api-0.0.2.dist-info/WHEEL +5 -0
- deepctl_cmd_api-0.0.2.dist-info/entry_points.txt +2 -0
- deepctl_cmd_api-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""API command for deepctl."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from deepctl_core import AuthManager, BaseCommand, Config, DeepgramClient
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
from .models import ApiResult
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
DEFAULT_BASE_URL = "https://api.deepgram.com"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApiCommand(BaseCommand):
|
|
22
|
+
"""Make authenticated requests to Deepgram APIs."""
|
|
23
|
+
|
|
24
|
+
name = "api"
|
|
25
|
+
help = "Make authenticated requests to Deepgram APIs"
|
|
26
|
+
short_help = "Make API requests"
|
|
27
|
+
|
|
28
|
+
requires_auth = True
|
|
29
|
+
requires_project = False
|
|
30
|
+
ci_friendly = True
|
|
31
|
+
|
|
32
|
+
examples = [
|
|
33
|
+
"dg api /v1/projects",
|
|
34
|
+
"dg api -X POST /v1/projects -f name=MyProject",
|
|
35
|
+
"dg api /v1/projects --jq '.projects[0].name'",
|
|
36
|
+
]
|
|
37
|
+
agent_help = (
|
|
38
|
+
"Make authenticated HTTP requests to any Deepgram REST API endpoint. "
|
|
39
|
+
"Works like curl with auto-injected credentials. Supports all HTTP methods, "
|
|
40
|
+
"body construction via -f key=value, and jq filtering of responses."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def get_arguments(self) -> list[dict[str, Any]]:
|
|
44
|
+
"""Get command arguments and options."""
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
"name": "endpoint",
|
|
48
|
+
"help": "API endpoint path (e.g. /v1/projects)",
|
|
49
|
+
"type": str,
|
|
50
|
+
"required": True,
|
|
51
|
+
"is_option": False,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"names": ["-X", "--method"],
|
|
55
|
+
"help": "HTTP method (default: GET)",
|
|
56
|
+
"type": str,
|
|
57
|
+
"default": "GET",
|
|
58
|
+
"is_option": True,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"names": ["-f", "--field"],
|
|
62
|
+
"help": (
|
|
63
|
+
"Request body field as key=value (string) or key:=value (raw JSON)"
|
|
64
|
+
),
|
|
65
|
+
"type": str,
|
|
66
|
+
"multiple": True,
|
|
67
|
+
"is_option": True,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"names": ["--input"],
|
|
71
|
+
"help": "Request body from file path or - for stdin",
|
|
72
|
+
"type": str,
|
|
73
|
+
"is_option": True,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"names": ["-H", "--header"],
|
|
77
|
+
"help": "Custom header as 'Key: Value'",
|
|
78
|
+
"type": str,
|
|
79
|
+
"multiple": True,
|
|
80
|
+
"is_option": True,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"names": ["--jq"],
|
|
84
|
+
"help": "jq filter expression to apply to response",
|
|
85
|
+
"type": str,
|
|
86
|
+
"is_option": True,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"names": ["--raw"],
|
|
90
|
+
"help": "Output raw response without formatting",
|
|
91
|
+
"is_flag": True,
|
|
92
|
+
"is_option": True,
|
|
93
|
+
},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
def _resolve_url(self, endpoint: str) -> str:
|
|
97
|
+
"""Resolve endpoint to full URL."""
|
|
98
|
+
if endpoint.startswith("http://") or endpoint.startswith("https://"):
|
|
99
|
+
return endpoint
|
|
100
|
+
base = DEFAULT_BASE_URL.rstrip("/")
|
|
101
|
+
path = endpoint if endpoint.startswith("/") else f"/{endpoint}"
|
|
102
|
+
return f"{base}{path}"
|
|
103
|
+
|
|
104
|
+
def _build_headers(
|
|
105
|
+
self,
|
|
106
|
+
api_key: str,
|
|
107
|
+
custom_headers: tuple[str, ...] | list[str] | None,
|
|
108
|
+
) -> dict[str, str]:
|
|
109
|
+
"""Build request headers with auth and custom headers."""
|
|
110
|
+
headers: dict[str, str] = {
|
|
111
|
+
"Authorization": f"Token {api_key}",
|
|
112
|
+
}
|
|
113
|
+
if custom_headers:
|
|
114
|
+
for header in custom_headers:
|
|
115
|
+
if ":" in header:
|
|
116
|
+
key, value = header.split(":", 1)
|
|
117
|
+
headers[key.strip()] = value.strip()
|
|
118
|
+
return headers
|
|
119
|
+
|
|
120
|
+
def _build_body(
|
|
121
|
+
self,
|
|
122
|
+
fields: tuple[str, ...] | list[str] | None,
|
|
123
|
+
input_source: str | None,
|
|
124
|
+
) -> Any | None:
|
|
125
|
+
"""Build request body from fields or input source."""
|
|
126
|
+
if fields and input_source:
|
|
127
|
+
raise ValueError("Cannot use both --field and --input. Choose one.")
|
|
128
|
+
|
|
129
|
+
if fields:
|
|
130
|
+
body: dict[str, Any] = {}
|
|
131
|
+
for field in fields:
|
|
132
|
+
if ":=" in field:
|
|
133
|
+
key, raw_value = field.split(":=", 1)
|
|
134
|
+
body[key] = json.loads(raw_value)
|
|
135
|
+
elif "=" in field:
|
|
136
|
+
key, value = field.split("=", 1)
|
|
137
|
+
body[key] = value
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"Invalid field format: {field}. "
|
|
141
|
+
f"Use key=value or key:=json_value"
|
|
142
|
+
)
|
|
143
|
+
return body
|
|
144
|
+
|
|
145
|
+
if input_source:
|
|
146
|
+
if input_source == "-":
|
|
147
|
+
raw = sys.stdin.read()
|
|
148
|
+
else:
|
|
149
|
+
with open(input_source) as f:
|
|
150
|
+
raw = f.read()
|
|
151
|
+
try:
|
|
152
|
+
return json.loads(raw)
|
|
153
|
+
except json.JSONDecodeError:
|
|
154
|
+
return raw
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _apply_jq_filter(self, response_text: str, jq_filter: str) -> str:
|
|
159
|
+
"""Apply jq filter to response text."""
|
|
160
|
+
if not shutil.which("jq"):
|
|
161
|
+
console.print(
|
|
162
|
+
Panel(
|
|
163
|
+
"[red]jq is not installed[/red]\n\n"
|
|
164
|
+
"[bold]To install jq:[/bold]\n"
|
|
165
|
+
" macOS: [dim]brew install jq[/dim]\n"
|
|
166
|
+
" Ubuntu/Debian: [dim]sudo apt install jq[/dim]\n"
|
|
167
|
+
" Windows: [dim]choco install jq[/dim]\n"
|
|
168
|
+
" Or visit: [link]https://jqlang.github.io/jq/download/[/link]",
|
|
169
|
+
title="jq Required",
|
|
170
|
+
border_style="red",
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
raise RuntimeError("jq is not installed")
|
|
174
|
+
|
|
175
|
+
result = subprocess.run(
|
|
176
|
+
["jq", jq_filter],
|
|
177
|
+
input=response_text,
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
)
|
|
181
|
+
if result.returncode != 0:
|
|
182
|
+
raise RuntimeError(f"jq error: {result.stderr.strip()}")
|
|
183
|
+
return result.stdout.strip()
|
|
184
|
+
|
|
185
|
+
def handle(
|
|
186
|
+
self,
|
|
187
|
+
config: Config,
|
|
188
|
+
auth_manager: AuthManager,
|
|
189
|
+
client: DeepgramClient,
|
|
190
|
+
**kwargs: Any,
|
|
191
|
+
) -> Any:
|
|
192
|
+
"""Handle API command execution."""
|
|
193
|
+
endpoint = kwargs.get("endpoint", "")
|
|
194
|
+
method = (kwargs.get("method") or "GET").upper()
|
|
195
|
+
fields = kwargs.get("field") or ()
|
|
196
|
+
input_source = kwargs.get("input")
|
|
197
|
+
custom_headers = kwargs.get("header") or ()
|
|
198
|
+
jq_filter = kwargs.get("jq")
|
|
199
|
+
raw_output = kwargs.get("raw", False)
|
|
200
|
+
|
|
201
|
+
# Resolve URL
|
|
202
|
+
url = self._resolve_url(endpoint)
|
|
203
|
+
|
|
204
|
+
# Get API key
|
|
205
|
+
api_key = auth_manager.get_api_key()
|
|
206
|
+
if not api_key:
|
|
207
|
+
console.print("[red]No API key found. Run 'deepctl login' first.[/red]")
|
|
208
|
+
return ApiResult(
|
|
209
|
+
status="error",
|
|
210
|
+
message="No API key found",
|
|
211
|
+
method=method,
|
|
212
|
+
url=url,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Build headers
|
|
216
|
+
headers = self._build_headers(api_key, custom_headers)
|
|
217
|
+
|
|
218
|
+
# Build body
|
|
219
|
+
try:
|
|
220
|
+
body = self._build_body(fields, input_source)
|
|
221
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
222
|
+
console.print(f"[red]Error building request body: {e}[/red]")
|
|
223
|
+
return ApiResult(
|
|
224
|
+
status="error",
|
|
225
|
+
message=str(e),
|
|
226
|
+
method=method,
|
|
227
|
+
url=url,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Prepare request kwargs
|
|
231
|
+
request_kwargs: dict[str, Any] = {
|
|
232
|
+
"method": method,
|
|
233
|
+
"url": url,
|
|
234
|
+
"headers": headers,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if body is not None:
|
|
238
|
+
if isinstance(body, (dict, list)):
|
|
239
|
+
request_kwargs["json"] = body
|
|
240
|
+
else:
|
|
241
|
+
request_kwargs["content"] = body
|
|
242
|
+
|
|
243
|
+
# Make request
|
|
244
|
+
try:
|
|
245
|
+
with httpx.Client(timeout=30.0) as http_client:
|
|
246
|
+
response = http_client.request(**request_kwargs)
|
|
247
|
+
except httpx.RequestError as e:
|
|
248
|
+
console.print(f"[red]Request failed: {e}[/red]")
|
|
249
|
+
return ApiResult(
|
|
250
|
+
status="error",
|
|
251
|
+
message=f"Request failed: {e}",
|
|
252
|
+
method=method,
|
|
253
|
+
url=url,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
elapsed_ms = response.elapsed.total_seconds() * 1000
|
|
257
|
+
|
|
258
|
+
# Handle response
|
|
259
|
+
response_text = response.text
|
|
260
|
+
response_body: Any = None
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
response_body = response.json()
|
|
264
|
+
except (json.JSONDecodeError, ValueError):
|
|
265
|
+
response_body = response_text
|
|
266
|
+
|
|
267
|
+
# Check for errors
|
|
268
|
+
if response.status_code >= 400:
|
|
269
|
+
console.print(f"[red]{method} {url} → {response.status_code}[/red]")
|
|
270
|
+
if isinstance(response_body, dict):
|
|
271
|
+
console.print_json(data=response_body)
|
|
272
|
+
elif response_text:
|
|
273
|
+
console.print(f"[dim]{response_text[:1000]}[/dim]")
|
|
274
|
+
|
|
275
|
+
return ApiResult(
|
|
276
|
+
status="error",
|
|
277
|
+
message=f"HTTP {response.status_code}",
|
|
278
|
+
method=method,
|
|
279
|
+
url=url,
|
|
280
|
+
status_code=response.status_code,
|
|
281
|
+
response_body=response_body,
|
|
282
|
+
elapsed_ms=elapsed_ms,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Apply jq filter if specified
|
|
286
|
+
if jq_filter:
|
|
287
|
+
try:
|
|
288
|
+
json_text = (
|
|
289
|
+
json.dumps(response_body)
|
|
290
|
+
if isinstance(response_body, (dict, list))
|
|
291
|
+
else response_text
|
|
292
|
+
)
|
|
293
|
+
filtered = self._apply_jq_filter(json_text, jq_filter)
|
|
294
|
+
console.print(filtered)
|
|
295
|
+
return ApiResult(
|
|
296
|
+
status="success",
|
|
297
|
+
method=method,
|
|
298
|
+
url=url,
|
|
299
|
+
status_code=response.status_code,
|
|
300
|
+
response_body=filtered,
|
|
301
|
+
elapsed_ms=elapsed_ms,
|
|
302
|
+
)
|
|
303
|
+
except RuntimeError as e:
|
|
304
|
+
console.print(f"[red]{e}[/red]")
|
|
305
|
+
return ApiResult(
|
|
306
|
+
status="error",
|
|
307
|
+
message=str(e),
|
|
308
|
+
method=method,
|
|
309
|
+
url=url,
|
|
310
|
+
status_code=response.status_code,
|
|
311
|
+
response_body=response_body,
|
|
312
|
+
elapsed_ms=elapsed_ms,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Output response
|
|
316
|
+
if raw_output:
|
|
317
|
+
console.print(response_text)
|
|
318
|
+
elif isinstance(response_body, (dict, list)):
|
|
319
|
+
console.print_json(data=response_body)
|
|
320
|
+
else:
|
|
321
|
+
console.print(response_text)
|
|
322
|
+
|
|
323
|
+
return ApiResult(
|
|
324
|
+
status="success",
|
|
325
|
+
method=method,
|
|
326
|
+
url=url,
|
|
327
|
+
status_code=response.status_code,
|
|
328
|
+
response_body=response_body,
|
|
329
|
+
elapsed_ms=elapsed_ms,
|
|
330
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Data models for API command."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from deepctl_core import BaseResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiResult(BaseResult):
|
|
9
|
+
"""Result from API command execution."""
|
|
10
|
+
|
|
11
|
+
method: str = ""
|
|
12
|
+
url: str = ""
|
|
13
|
+
status_code: int = 0
|
|
14
|
+
response_body: Any = None
|
|
15
|
+
elapsed_ms: float | None = None
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepctl-cmd-api
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: API command for deepctl
|
|
5
|
+
Author-email: Deepgram <devrel@deepgram.com>
|
|
6
|
+
Maintainer-email: Deepgram <devrel@deepgram.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: deepgram,cli,api,rest
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: deepctl-core>=0.1.10
|
|
18
|
+
Requires-Dist: click>=8.0.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Requires-Dist: httpx>=0.24.0
|
|
22
|
+
|
|
23
|
+
# deepctl-cmd-api
|
|
24
|
+
|
|
25
|
+
> Part of [deepctl](https://github.com/deepgram/cli) — Official Deepgram CLI
|
|
26
|
+
|
|
27
|
+
API command for deepctl
|
|
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 api` | `deepctl_cmd_api.command:ApiCommand` |
|
|
55
|
+
|
|
56
|
+
## Dependencies
|
|
57
|
+
|
|
58
|
+
- `click>=8.0.0`
|
|
59
|
+
- `rich>=13.0.0`
|
|
60
|
+
- `pydantic>=2.0.0`
|
|
61
|
+
- `httpx>=0.24.0`
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT — see [LICENSE](../../LICENSE)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
deepctl_cmd_api/__init__.py,sha256=-tM5NMfD0YOfZPWsJOeCV2-9RWfbiA-Rhqs9_354FRA,90
|
|
2
|
+
deepctl_cmd_api/command.py,sha256=a6ZBdsShkj0H-P2qobEnKw9yXqfTY-T-W005z27rwYY,10812
|
|
3
|
+
deepctl_cmd_api/models.py,sha256=jZJX4K1UFsdjyJaQXHJphUhEb1zDU3VMIAQpNmsBLqQ,303
|
|
4
|
+
deepctl_cmd_api-0.0.2.dist-info/METADATA,sha256=3GiwMpzxjtjP1_8BO5BDbJGdDEv4WLFjtGE3Qwu4_wA,1472
|
|
5
|
+
deepctl_cmd_api-0.0.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
deepctl_cmd_api-0.0.2.dist-info/entry_points.txt,sha256=STXtVqiEWvWCfWYrBfIgnGsdvNH09knfiXVB4Azh91U,60
|
|
7
|
+
deepctl_cmd_api-0.0.2.dist-info/top_level.txt,sha256=eR7egK4KtMzd_nWbn-3f6GH_aIAyGYTVdlg1HIlln4c,16
|
|
8
|
+
deepctl_cmd_api-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deepctl_cmd_api
|