deepctl-cmd-usage 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.
@@ -0,0 +1,6 @@
1
+ """Usage command package for deepctl."""
2
+
3
+ from .command import UsageCommand
4
+ from .models import UsageBucket, UsageResult
5
+
6
+ __all__ = ["UsageBucket", "UsageCommand", "UsageResult"]
@@ -0,0 +1,401 @@
1
+ """Usage command for deepctl."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Any
5
+
6
+ from deepctl_core import (
7
+ AuthManager,
8
+ BaseCommand,
9
+ BaseResult,
10
+ Config,
11
+ DeepgramClient,
12
+ )
13
+ from deepctl_shared_utils import validate_date_format
14
+ from rich.console import Console
15
+
16
+ from .models import UsageBucket, UsageResult
17
+
18
+ console = Console()
19
+
20
+
21
+ class UsageCommand(BaseCommand):
22
+ """Command for viewing Deepgram usage statistics."""
23
+
24
+ name = "usage"
25
+ help = "View Deepgram usage statistics"
26
+ short_help = "View usage statistics"
27
+
28
+ # Usage requires authentication and project
29
+ requires_auth = True
30
+ requires_project = True
31
+ ci_friendly = True
32
+
33
+ def get_arguments(self) -> list[dict[str, Any]]:
34
+ """Get command arguments and options."""
35
+ return [
36
+ {
37
+ "names": ["--project-id", "-p"],
38
+ "help": "Project ID (uses configured project if not provided)",
39
+ "type": str,
40
+ "is_option": True,
41
+ },
42
+ {
43
+ "names": ["--start-date", "-s"],
44
+ "help": "Start date (YYYY-MM-DD or ISO format)",
45
+ "type": str,
46
+ "is_option": True,
47
+ },
48
+ {
49
+ "names": ["--end-date", "-e"],
50
+ "help": "End date (YYYY-MM-DD or ISO format)",
51
+ "type": str,
52
+ "is_option": True,
53
+ },
54
+ {
55
+ "names": ["--last-week"],
56
+ "help": "Show usage for last week",
57
+ "is_flag": True,
58
+ "is_option": True,
59
+ },
60
+ {
61
+ "names": ["--last-month"],
62
+ "help": "Show usage for last month",
63
+ "is_flag": True,
64
+ "is_option": True,
65
+ },
66
+ {
67
+ "names": ["--current-month"],
68
+ "help": "Show usage for current month",
69
+ "is_flag": True,
70
+ "is_option": True,
71
+ },
72
+ {
73
+ "names": ["--summary"],
74
+ "help": "Show summary only",
75
+ "is_flag": True,
76
+ "is_option": True,
77
+ },
78
+ ]
79
+
80
+ def handle(
81
+ self,
82
+ config: Config,
83
+ auth_manager: AuthManager,
84
+ client: DeepgramClient,
85
+ **kwargs: Any,
86
+ ) -> BaseResult:
87
+ """Handle usage command."""
88
+ project_id = kwargs.get("project_id")
89
+ start_date = kwargs.get("start_date")
90
+ end_date = kwargs.get("end_date")
91
+ last_week = kwargs.get("last_week", False)
92
+ last_month = kwargs.get("last_month", False)
93
+ current_month = kwargs.get("current_month", False)
94
+ summary_only = kwargs.get("summary", False)
95
+
96
+ try:
97
+ # Determine date range
98
+ if last_week:
99
+ start_date, end_date = self._get_last_week_range()
100
+ console.print("[blue]Fetching usage for last week...[/blue]")
101
+ elif last_month:
102
+ start_date, end_date = self._get_last_month_range()
103
+ console.print("[blue]Fetching usage for last month...[/blue]")
104
+ elif current_month:
105
+ start_date, end_date = self._get_current_month_range()
106
+ console.print(
107
+ "[blue]Fetching usage for current month...[/blue]"
108
+ )
109
+ elif start_date or end_date:
110
+ # Validate custom date range
111
+ if start_date and not validate_date_format(start_date):
112
+ return BaseResult(
113
+ status="error",
114
+ message=f"Invalid start date format: {start_date}",
115
+ )
116
+ if end_date and not validate_date_format(end_date):
117
+ return BaseResult(
118
+ status="error",
119
+ message=f"Invalid end date format: {end_date}",
120
+ )
121
+
122
+ console.print(
123
+ f"[blue]Fetching usage from "
124
+ f"{start_date or 'beginning'} to "
125
+ f"{end_date or 'now'}...[/blue]"
126
+ )
127
+ else:
128
+ # Default to current month
129
+ start_date, end_date = self._get_current_month_range()
130
+ console.print(
131
+ "[blue]Fetching usage for current month...[/blue]"
132
+ )
133
+
134
+ # Get usage data
135
+ result = client.get_usage(project_id, start_date, end_date)
136
+
137
+ # Process and display results
138
+ return self._process_usage_result(
139
+ result, summary_only, start_date, end_date
140
+ )
141
+
142
+ except Exception as e:
143
+ console.print(f"[red]Error fetching usage:[/red] {e}")
144
+ return BaseResult(status="error", message=str(e))
145
+
146
+ def _get_last_week_range(self) -> tuple[str, str]:
147
+ """Get date range for last week."""
148
+ today = datetime.now()
149
+ last_week = today - timedelta(days=7)
150
+ return last_week.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d")
151
+
152
+ def _get_last_month_range(self) -> tuple[str, str]:
153
+ """Get date range for last month."""
154
+ today = datetime.now()
155
+ last_month = today - timedelta(days=30)
156
+ return last_month.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d")
157
+
158
+ def _get_current_month_range(self) -> tuple[str, str]:
159
+ """Get date range for current month."""
160
+ today = datetime.now()
161
+ first_day = today.replace(day=1)
162
+ return first_day.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d")
163
+
164
+ def _process_usage_result(
165
+ self,
166
+ result: Any,
167
+ summary_only: bool,
168
+ start_date: str | None,
169
+ end_date: str | None,
170
+ ) -> BaseResult:
171
+ """Process usage result and display formatted output."""
172
+ try:
173
+ # Handle UsageSummaryResponse object - try different
174
+ # conversion methods
175
+ result_dict = None
176
+
177
+ if hasattr(result, "to_dict"):
178
+ result_dict = result.to_dict()
179
+ elif hasattr(result, "dict"):
180
+ result_dict = result.dict()
181
+ elif hasattr(result, "__dict__"):
182
+ # For SDK response objects, we need to access attributes
183
+ # directly
184
+ result_dict = {
185
+ "results": (
186
+ result.results if hasattr(result, "results") else []
187
+ ),
188
+ "start": (
189
+ result.start
190
+ if hasattr(result, "start")
191
+ else start_date
192
+ ),
193
+ "end": result.end if hasattr(result, "end") else end_date,
194
+ "project_id": (
195
+ result.project_id
196
+ if hasattr(result, "project_id")
197
+ else ""
198
+ ),
199
+ }
200
+ else:
201
+ result_dict = result
202
+
203
+ # Extract usage data
204
+ if (
205
+ result_dict
206
+ and "results" in result_dict
207
+ and isinstance(result_dict["results"], list)
208
+ ):
209
+ # Calculate totals from results array
210
+ total_hours = 0
211
+ total_requests = 0
212
+ total_tts_characters = 0
213
+ total_tokens_out = 0
214
+
215
+ buckets: list[UsageBucket] = []
216
+
217
+ for item in result_dict["results"]:
218
+ # Sum up totals
219
+ if "total_hours" in item:
220
+ total_hours += item["total_hours"]
221
+ if "requests" in item:
222
+ total_requests += item["requests"]
223
+ if "tts" in item and "characters" in item["tts"]:
224
+ total_tts_characters += item["tts"]["characters"]
225
+ if "tokens" in item and "out" in item["tokens"]:
226
+ total_tokens_out += item["tokens"]["out"]
227
+
228
+ # Create bucket for each day
229
+ buckets.append(
230
+ UsageBucket(
231
+ start=item.get("start", ""),
232
+ end=item.get("end", ""),
233
+ hours=float(item.get("total_hours", 0)),
234
+ )
235
+ )
236
+
237
+ # Display summary
238
+ console.print(
239
+ f"\n[green]Usage Summary ({start_date} to "
240
+ f"{end_date}):[/green]"
241
+ )
242
+ console.print(f" Total Hours: {total_hours:,.1f}")
243
+ console.print(f" Total Requests: {total_requests:,}")
244
+
245
+ if total_tts_characters > 0:
246
+ console.print(
247
+ f" TTS Characters: {total_tts_characters:,}"
248
+ )
249
+
250
+ if total_tokens_out > 0:
251
+ console.print(f" Tokens Out: {total_tokens_out:,}")
252
+
253
+ # Display detailed breakdown if not summary only
254
+ if not summary_only and result_dict["results"]:
255
+ console.print("\n[blue]Daily Breakdown:[/blue]")
256
+ for item in result_dict["results"]:
257
+ item_date = item.get("start", "Unknown")
258
+ hours = item.get("total_hours", 0)
259
+ requests = item.get("requests", 0)
260
+
261
+ console.print(f"\n {item_date}:")
262
+ console.print(f" Hours: {hours}")
263
+ console.print(f" Requests: {requests}")
264
+
265
+ if "tts" in item:
266
+ console.print(
267
+ f" TTS Characters: "
268
+ f"{item['tts'].get('characters', 0):,}"
269
+ )
270
+ console.print(
271
+ f" TTS Requests: "
272
+ f"{item['tts'].get('requests', 0):,}"
273
+ )
274
+
275
+ if (
276
+ "tokens" in item
277
+ and item["tokens"].get("out", 0) > 0
278
+ ):
279
+ console.print(
280
+ f" Tokens Out: "
281
+ f"{item['tokens'].get('out', 0):,}"
282
+ )
283
+
284
+ project_id = result_dict.get("project_id", "")
285
+ return UsageResult(
286
+ status="success",
287
+ project_id=project_id,
288
+ buckets=buckets,
289
+ total_hours=float(total_hours),
290
+ )
291
+ else:
292
+ console.print(
293
+ "[yellow]No usage data found for the specified "
294
+ "period[/yellow]"
295
+ )
296
+ return BaseResult(status="info", message="No usage data found")
297
+
298
+ except Exception as e:
299
+ console.print(f"[red]Error processing usage data:[/red] {e}")
300
+ import traceback
301
+
302
+ traceback.print_exc()
303
+ return BaseResult(status="error", message=str(e))
304
+
305
+ def _extract_usage_data(self, result: dict[str, Any]) -> dict[str, Any]:
306
+ """Extract usage data from API response."""
307
+ if "usage" in result:
308
+ return dict(result["usage"])
309
+ elif "results" in result:
310
+ return dict(result["results"])
311
+ else:
312
+ return result
313
+
314
+ def _display_usage_summary(
315
+ self, usage_data: dict[str, Any], start_date: str, end_date: str
316
+ ) -> None:
317
+ """Display usage summary."""
318
+ console.print(
319
+ f"\n[green]Usage Summary ({start_date} to " f"{end_date}):[/green]"
320
+ )
321
+
322
+ # Try to extract common usage metrics
323
+ total_requests = usage_data.get("requests", 0)
324
+ total_duration = usage_data.get("duration", 0)
325
+ total_cost = usage_data.get("cost", 0)
326
+
327
+ if total_requests:
328
+ console.print(f" Total Requests: {total_requests:,}")
329
+
330
+ if total_duration:
331
+ if isinstance(total_duration, int | float):
332
+ hours = total_duration / 3600
333
+ console.print(
334
+ f" Total Duration: {hours:.2f} hours "
335
+ f"({total_duration:,} seconds)"
336
+ )
337
+ else:
338
+ console.print(f" Total Duration: {total_duration}")
339
+
340
+ if total_cost:
341
+ console.print(f" Total Cost: ${total_cost:.2f}")
342
+
343
+ # Display any other summary metrics
344
+ for key, value in usage_data.items():
345
+ if key not in [
346
+ "requests",
347
+ "duration",
348
+ "cost",
349
+ "details",
350
+ "breakdown",
351
+ ]:
352
+ if isinstance(value, int | float):
353
+ console.print(
354
+ f" {key.replace('_', ' ').title()}: " f"{value:,}"
355
+ )
356
+ else:
357
+ console.print(
358
+ f" {key.replace('_', ' ').title()}: {value}"
359
+ )
360
+
361
+ def _display_usage_details(self, usage_data: dict[str, Any]) -> None:
362
+ """Display detailed usage breakdown."""
363
+ console.print("\n[blue]Detailed Breakdown:[/blue]")
364
+
365
+ # Look for detailed breakdown data
366
+ details = usage_data.get("details", usage_data.get("breakdown", {}))
367
+
368
+ if details and isinstance(details, dict):
369
+ for category, data in details.items():
370
+ console.print(f"\n {category.replace('_', ' ').title()}:")
371
+
372
+ if isinstance(data, dict):
373
+ for key, value in data.items():
374
+ if isinstance(value, int | float):
375
+ console.print(
376
+ f" {key.replace('_', ' ').title()}: "
377
+ f"{value:,}"
378
+ )
379
+ else:
380
+ console.print(
381
+ f" {key.replace('_', ' ').title()}: {value}"
382
+ )
383
+ else:
384
+ console.print(f" {data}")
385
+ else:
386
+ console.print(" No detailed breakdown available")
387
+
388
+ def _format_duration(self, seconds: float) -> str:
389
+ """Format duration in seconds to human readable format."""
390
+ if seconds < 60:
391
+ return f"{seconds:.1f} seconds"
392
+ elif seconds < 3600:
393
+ minutes = seconds / 60
394
+ return f"{minutes:.1f} minutes"
395
+ else:
396
+ hours = seconds / 3600
397
+ return f"{hours:.2f} hours"
398
+
399
+ def _format_cost(self, cost: float) -> str:
400
+ """Format cost with currency symbol."""
401
+ return f"${cost:.2f}"
@@ -0,0 +1,16 @@
1
+ """Models for usage command."""
2
+
3
+ from deepctl_core import BaseResult
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class UsageBucket(BaseModel):
8
+ start: str # ISO date
9
+ end: str
10
+ hours: float
11
+
12
+
13
+ class UsageResult(BaseResult):
14
+ project_id: str
15
+ buckets: list[UsageBucket] = Field(default_factory=list)
16
+ total_hours: float = 0.0
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepctl-cmd-usage
3
+ Version: 0.1.0
4
+ Summary: Usage 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,usage,statistics
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.0
18
+ Requires-Dist: deepctl-shared-utils>=0.1.0
19
+ Requires-Dist: click>=8.0.0
20
+ Requires-Dist: rich>=13.0.0
21
+ Requires-Dist: deepgram-sdk>=3.0.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+
24
+ # deepctl-cmd-usage
25
+
26
+ Usage command for deepctl. This package is an internal component of deepctl and is not intended for standalone use.
@@ -0,0 +1,8 @@
1
+ deepctl_cmd_usage/__init__.py,sha256=8zT9G12w_dRsg0CAj1Y6SmYLQ0fxg0sHrtbQd9uHHPs,179
2
+ deepctl_cmd_usage/command.py,sha256=y8OzAvInHpVPp4ocw8A28M3dUxRkTX9Oc7d-3xoaf2w,15228
3
+ deepctl_cmd_usage/models.py,sha256=E9uONsebVxcke5ILSocH8fFXbZ67DpPy_ZMtHBySf3Y,339
4
+ deepctl_cmd_usage-0.1.0.dist-info/METADATA,sha256=wEwJehb4OHbyxSZ6TExM9myKzXU2vL2bUeSdTUToOZ4,945
5
+ deepctl_cmd_usage-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ deepctl_cmd_usage-0.1.0.dist-info/entry_points.txt,sha256=_WIR7LHQie7L89ArgDrbZHRCJmly-SVpFA6zkBen1-E,66
7
+ deepctl_cmd_usage-0.1.0.dist-info/top_level.txt,sha256=u_mNe0sNtHxtMSn5WXSojRTqXqPMNt7e-wRb0F87DfQ,18
8
+ deepctl_cmd_usage-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [deepctl.commands]
2
+ usage = deepctl_cmd_usage.command:UsageCommand
@@ -0,0 +1 @@
1
+ deepctl_cmd_usage