deepctl-shared-utils 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,13 @@
1
+ """Shared utilities for deepctl."""
2
+
3
+ from .models import FileInfo
4
+ from .validation import validate_audio_file, validate_date_format, validate_url
5
+
6
+ __all__ = [
7
+ "FileInfo",
8
+ "validate_audio_file",
9
+ "validate_date_format",
10
+ "validate_url",
11
+ ]
12
+
13
+ __version__ = "0.1.0"
@@ -0,0 +1,19 @@
1
+ """Models for shared utilities."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class FileInfo(BaseModel):
7
+ """File information for validation results."""
8
+
9
+ path: str
10
+ name: str | None = None
11
+ extension: str | None = None
12
+ size_bytes: int | None = None
13
+ size_mb: float | None = None
14
+ modified: float | None = None
15
+ readable: bool | None = None
16
+ exists: bool = False
17
+ is_file: bool | None = None
18
+ is_audio: bool | None = None
19
+ error: str | None = None
@@ -0,0 +1 @@
1
+ # This file indicates that this package supports type hints
@@ -0,0 +1,543 @@
1
+ """Input validation utilities for deepctl."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from urllib.parse import urlparse
7
+
8
+ import httpx
9
+ from rich.console import Console
10
+
11
+ from .models import FileInfo
12
+
13
+ console = Console()
14
+
15
+ # Supported audio file extensions
16
+ SUPPORTED_AUDIO_EXTENSIONS = {
17
+ ".mp3",
18
+ ".wav",
19
+ ".flac",
20
+ ".m4a",
21
+ ".aac",
22
+ ".ogg",
23
+ ".wma",
24
+ ".opus",
25
+ ".amr",
26
+ ".3gp",
27
+ ".mp4",
28
+ ".mov",
29
+ ".avi",
30
+ ".mkv",
31
+ ".webm",
32
+ }
33
+
34
+ # Common audio MIME types
35
+ AUDIO_MIME_TYPES = {
36
+ "audio/mpeg",
37
+ "audio/wav",
38
+ "audio/flac",
39
+ "audio/m4a",
40
+ "audio/aac",
41
+ "audio/ogg",
42
+ "audio/opus",
43
+ "audio/amr",
44
+ "audio/3gp",
45
+ "audio/mp4",
46
+ "video/mp4",
47
+ "video/quicktime",
48
+ "video/x-msvideo",
49
+ "video/x-matroska",
50
+ "video/webm",
51
+ }
52
+
53
+
54
+ def validate_audio_file(file_path: str | Path) -> bool:
55
+ """Validate that a file exists and appears to be an audio file.
56
+
57
+ Args:
58
+ file_path: Path to the file
59
+
60
+ Returns:
61
+ True if file is valid audio file, False otherwise
62
+ """
63
+ try:
64
+ path = Path(file_path)
65
+
66
+ # Check if file exists
67
+ if not path.exists():
68
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
69
+ return False
70
+
71
+ # Check if it's a file (not directory)
72
+ if not path.is_file():
73
+ console.print(f"[red]Error:[/red] Path is not a file: {file_path}")
74
+ return False
75
+
76
+ # Check file extension
77
+ extension = path.suffix.lower()
78
+ if extension not in SUPPORTED_AUDIO_EXTENSIONS:
79
+ console.print(
80
+ f"[yellow]Warning:[/yellow] File extension '{extension}' "
81
+ f"is not in supported list"
82
+ )
83
+ console.print(
84
+ f"[dim]Supported extensions: "
85
+ f"{', '.join(sorted(SUPPORTED_AUDIO_EXTENSIONS))}[/dim]"
86
+ )
87
+ return False
88
+
89
+ # Check if file is readable
90
+ if not os.access(path, os.R_OK):
91
+ console.print(
92
+ f"[red]Error:[/red] File is not readable: {file_path}"
93
+ )
94
+ return False
95
+
96
+ # Check file size (warn if very large)
97
+ size_mb = path.stat().st_size / (1024 * 1024)
98
+ if size_mb > 500: # 500 MB
99
+ console.print(
100
+ f"[yellow]Warning:[/yellow] Large file detected "
101
+ f"({size_mb:.1f} MB)"
102
+ )
103
+ console.print("[dim]Large files may take longer to process[/dim]")
104
+
105
+ return True
106
+
107
+ except Exception as e:
108
+ console.print(f"[red]Error validating file:[/red] {e}")
109
+ return False
110
+
111
+
112
+ def validate_url(url: str, check_accessibility: bool = True) -> bool:
113
+ """Validate URL format and optionally check accessibility.
114
+
115
+ Args:
116
+ url: URL to validate
117
+ check_accessibility: Whether to check if URL is accessible
118
+
119
+ Returns:
120
+ True if URL is valid, False otherwise
121
+ """
122
+ try:
123
+ # Parse URL
124
+ parsed = urlparse(url)
125
+
126
+ # Check basic URL structure
127
+ if not parsed.scheme or not parsed.netloc:
128
+ console.print(f"[red]Error:[/red] Invalid URL format: {url}")
129
+ return False
130
+
131
+ # Check scheme
132
+ if parsed.scheme not in ("http", "https"):
133
+ console.print(
134
+ f"[red]Error:[/red] Only HTTP/HTTPS URLs are supported: {url}"
135
+ )
136
+ return False
137
+
138
+ # Check if URL looks like an audio file
139
+ path = parsed.path.lower()
140
+ if path:
141
+ extension = Path(path).suffix
142
+ if extension and extension not in SUPPORTED_AUDIO_EXTENSIONS:
143
+ console.print(
144
+ f"[yellow]Warning:[/yellow] URL doesn't appear to be an "
145
+ f"audio file: {url}"
146
+ )
147
+ console.print(
148
+ f"[dim]Expected extensions: "
149
+ f"{', '.join(sorted(SUPPORTED_AUDIO_EXTENSIONS))}[/dim]"
150
+ )
151
+
152
+ # Check accessibility if requested
153
+ if check_accessibility:
154
+ return _check_url_accessibility(url)
155
+
156
+ return True
157
+
158
+ except Exception as e:
159
+ console.print(f"[red]Error validating URL:[/red] {e}")
160
+ return False
161
+
162
+
163
+ def _check_url_accessibility(url: str) -> bool:
164
+ """Check if URL is accessible.
165
+
166
+ Args:
167
+ url: URL to check
168
+
169
+ Returns:
170
+ True if accessible, False otherwise
171
+ """
172
+ try:
173
+ console.print("[dim]Checking URL accessibility...[/dim]")
174
+
175
+ with httpx.Client(timeout=10.0) as client:
176
+ # Use HEAD request to check without downloading
177
+ response = client.head(url, follow_redirects=True)
178
+
179
+ if response.status_code == 200:
180
+ # Check content type if available
181
+ content_type = response.headers.get("content-type", "").lower()
182
+ if content_type and not any(
183
+ mime in content_type for mime in AUDIO_MIME_TYPES
184
+ ):
185
+ console.print(
186
+ f"[yellow]Warning:[/yellow] Content type may not be "
187
+ f"audio: {content_type}"
188
+ )
189
+
190
+ # Check content length if available
191
+ content_length = response.headers.get("content-length")
192
+ if content_length:
193
+ size_mb = int(content_length) / (1024 * 1024)
194
+ if size_mb > 500: # 500 MB
195
+ console.print(
196
+ f"[yellow]Warning:[/yellow] Large file detected "
197
+ f"({size_mb:.1f} MB)"
198
+ )
199
+
200
+ return True
201
+ else:
202
+ console.print(
203
+ f"[red]Error:[/red] URL not accessible "
204
+ f"(HTTP {response.status_code}): {url}"
205
+ )
206
+ return False
207
+
208
+ except httpx.TimeoutException:
209
+ console.print(f"[red]Error:[/red] URL request timed out: {url}")
210
+ return False
211
+ except httpx.RequestError as e:
212
+ console.print(f"[red]Error:[/red] Failed to access URL: {e}")
213
+ return False
214
+ except Exception as e:
215
+ console.print(f"[red]Error checking URL accessibility:[/red] {e}")
216
+ return False
217
+
218
+
219
+ def validate_api_key(api_key: str) -> bool:
220
+ """Validate API key format.
221
+
222
+ Args:
223
+ api_key: API key to validate
224
+
225
+ Returns:
226
+ True if format is valid, False otherwise
227
+ """
228
+ if not api_key:
229
+ console.print("[red]Error:[/red] API key cannot be empty")
230
+ return False
231
+
232
+ # Check basic format
233
+ if not api_key.startswith(("sk-", "pk-")):
234
+ console.print(
235
+ "[yellow]Warning:[/yellow] API key doesn't match expected "
236
+ "format (should start with 'sk-' or 'pk-')"
237
+ )
238
+ return False
239
+
240
+ # Check length (Deepgram API keys are typically longer)
241
+ if len(api_key) < 20:
242
+ console.print("[yellow]Warning:[/yellow] API key seems too short")
243
+ return False
244
+
245
+ # Check for invalid characters
246
+ if not re.match(r"^[a-zA-Z0-9_-]+$", api_key):
247
+ console.print("[red]Error:[/red] API key contains invalid characters")
248
+ return False
249
+
250
+ return True
251
+
252
+
253
+ def validate_project_id(project_id: str) -> bool:
254
+ """Validate project ID format.
255
+
256
+ Args:
257
+ project_id: Project ID to validate
258
+
259
+ Returns:
260
+ True if format is valid, False otherwise
261
+ """
262
+ if not project_id:
263
+ console.print("[red]Error:[/red] Project ID cannot be empty")
264
+ return False
265
+
266
+ # Check if it's a valid UUID format
267
+ uuid_pattern = (
268
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
269
+ )
270
+ if not re.match(uuid_pattern, project_id, re.IGNORECASE):
271
+ console.print(
272
+ "[yellow]Warning:[/yellow] Project ID doesn't match expected "
273
+ "UUID format"
274
+ )
275
+ return False
276
+
277
+ return True
278
+
279
+
280
+ def validate_language_code(language: str) -> bool:
281
+ """Validate language code format.
282
+
283
+ Args:
284
+ language: Language code to validate
285
+
286
+ Returns:
287
+ True if format is valid, False otherwise
288
+ """
289
+ if not language:
290
+ return False
291
+
292
+ # Common language codes supported by Deepgram
293
+ supported_languages = {
294
+ "en-US",
295
+ "en-GB",
296
+ "en-AU",
297
+ "en-NZ",
298
+ "en-IN",
299
+ "es-ES",
300
+ "es-US",
301
+ "es-419",
302
+ "fr-FR",
303
+ "fr-CA",
304
+ "de-DE",
305
+ "de-CH",
306
+ "it-IT",
307
+ "pt-BR",
308
+ "pt-PT",
309
+ "ru-RU",
310
+ "tr-TR",
311
+ "hi-IN",
312
+ "ja-JP",
313
+ "ko-KR",
314
+ "zh-CN",
315
+ "zh-TW",
316
+ "nl-NL",
317
+ "pl-PL",
318
+ "sv-SE",
319
+ "no-NO",
320
+ "da-DK",
321
+ "fi-FI",
322
+ "uk-UA",
323
+ "el-GR",
324
+ "cs-CZ",
325
+ "hu-HU",
326
+ "ro-RO",
327
+ "sk-SK",
328
+ "sl-SI",
329
+ "bg-BG",
330
+ "hr-HR",
331
+ "et-EE",
332
+ "lv-LV",
333
+ "lt-LT",
334
+ "mt-MT",
335
+ }
336
+
337
+ if language not in supported_languages:
338
+ console.print(
339
+ f"[yellow]Warning:[/yellow] Language '{language}' may not be "
340
+ f"supported"
341
+ )
342
+ console.print(
343
+ "[dim]Common supported languages: en-US, es-ES, fr-FR, de-DE, "
344
+ "it-IT, pt-BR, ja-JP, ko-KR, zh-CN[/dim]"
345
+ )
346
+ return False
347
+
348
+ return True
349
+
350
+
351
+ def validate_model_name(model: str) -> bool:
352
+ """Validate model name.
353
+
354
+ Args:
355
+ model: Model name to validate
356
+
357
+ Returns:
358
+ True if format is valid, False otherwise
359
+ """
360
+ if not model:
361
+ return False
362
+
363
+ # Common Deepgram models
364
+ supported_models = {
365
+ "nova-2",
366
+ "nova",
367
+ "enhanced",
368
+ "base",
369
+ "meeting",
370
+ "phonecall",
371
+ "voicemail",
372
+ "finance",
373
+ "conversationalai",
374
+ "video",
375
+ "nova-2-general",
376
+ "nova-2-meeting",
377
+ "nova-2-phonecall",
378
+ "nova-2-voicemail",
379
+ "nova-2-finance",
380
+ "nova-2-conversationalai",
381
+ "nova-2-video",
382
+ "whisper-tiny",
383
+ "whisper-base",
384
+ "whisper-small",
385
+ "whisper-medium",
386
+ "whisper-large",
387
+ }
388
+
389
+ if model not in supported_models:
390
+ console.print(
391
+ f"[yellow]Warning:[/yellow] Model '{model}' may not be supported"
392
+ )
393
+ console.print(
394
+ "[dim]Common supported models: nova-2, nova, enhanced, base, "
395
+ "meeting, phonecall[/dim]"
396
+ )
397
+ return False
398
+
399
+ return True
400
+
401
+
402
+ def validate_date_format(date_str: str) -> bool:
403
+ """Validate date format (ISO 8601).
404
+
405
+ Args:
406
+ date_str: Date string to validate
407
+
408
+ Returns:
409
+ True if format is valid, False otherwise
410
+ """
411
+ if not date_str:
412
+ return False
413
+
414
+ # ISO 8601 date format patterns
415
+ patterns = [
416
+ r"^\d{4}-\d{2}-\d{2}$", # YYYY-MM-DD
417
+ r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$", # YYYY-MM-DDTHH:MM:SS
418
+ r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", # YYYY-MM-DDTHH:MM:SSZ
419
+ # YYYY-MM-DDTHH:MM:SS+TZ
420
+ r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$",
421
+ ]
422
+
423
+ for pattern in patterns:
424
+ if re.match(pattern, date_str):
425
+ return True
426
+
427
+ console.print(f"[red]Error:[/red] Invalid date format: {date_str}")
428
+ console.print(
429
+ "[dim]Expected formats: YYYY-MM-DD, YYYY-MM-DDTHH:MM:SS, or "
430
+ "YYYY-MM-DDTHH:MM:SSZ[/dim]"
431
+ )
432
+ return False
433
+
434
+
435
+ def validate_file_permissions(file_path: str | Path) -> bool:
436
+ """Validate file permissions.
437
+
438
+ Args:
439
+ file_path: Path to check
440
+
441
+ Returns:
442
+ True if permissions are valid, False otherwise
443
+ """
444
+ try:
445
+ path = Path(file_path)
446
+
447
+ # Check read permission
448
+ if not os.access(path, os.R_OK):
449
+ console.print(
450
+ f"[red]Error:[/red] No read permission for file: {file_path}"
451
+ )
452
+ return False
453
+
454
+ # Check if file is not empty
455
+ if path.stat().st_size == 0:
456
+ console.print(f"[red]Error:[/red] File is empty: {file_path}")
457
+ return False
458
+
459
+ return True
460
+
461
+ except Exception as e:
462
+ console.print(f"[red]Error checking file permissions:[/red] {e}")
463
+ return False
464
+
465
+
466
+ def sanitize_filename(filename: str) -> str:
467
+ """Sanitize filename for cross-platform compatibility.
468
+
469
+ Args:
470
+ filename: Original filename
471
+
472
+ Returns:
473
+ Sanitized filename
474
+ """
475
+ # Remove invalid characters
476
+ sanitized = re.sub(r'[<>:"/\\|?*]', "_", filename)
477
+
478
+ # Remove leading/trailing whitespace and dots
479
+ sanitized = sanitized.strip(" .")
480
+
481
+ # Ensure filename is not empty
482
+ if not sanitized:
483
+ sanitized = "unnamed_file"
484
+
485
+ # Truncate if too long
486
+ if len(sanitized) > 255:
487
+ sanitized = sanitized[:255]
488
+
489
+ return sanitized
490
+
491
+
492
+ def validate_output_format(format_type: str) -> bool:
493
+ """Validate output format.
494
+
495
+ Args:
496
+ format_type: Format to validate
497
+
498
+ Returns:
499
+ True if format is valid, False otherwise
500
+ """
501
+ supported_formats = {"json", "yaml", "table", "csv"}
502
+
503
+ if format_type not in supported_formats:
504
+ console.print(
505
+ f"[red]Error:[/red] Unsupported output format: {format_type}"
506
+ )
507
+ console.print(
508
+ f"[dim]Supported formats: "
509
+ f"{', '.join(sorted(supported_formats))}[/dim]"
510
+ )
511
+ return False
512
+
513
+ return True
514
+
515
+
516
+ def get_file_info(file_path: str | Path) -> FileInfo:
517
+ """Get detailed information about a file.
518
+
519
+ Args:
520
+ file_path: Path to the file
521
+
522
+ Returns:
523
+ FileInfo model with file information
524
+ """
525
+ try:
526
+ path = Path(file_path)
527
+ stat = path.stat()
528
+
529
+ return FileInfo(
530
+ path=str(path.absolute()),
531
+ name=path.name,
532
+ extension=path.suffix.lower(),
533
+ size_bytes=stat.st_size,
534
+ size_mb=stat.st_size / (1024 * 1024),
535
+ modified=stat.st_mtime,
536
+ readable=os.access(path, os.R_OK),
537
+ exists=path.exists(),
538
+ is_file=path.is_file(),
539
+ is_audio=path.suffix.lower() in SUPPORTED_AUDIO_EXTENSIONS,
540
+ )
541
+
542
+ except Exception as e:
543
+ return FileInfo(path=str(file_path), error=str(e), exists=False)
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepctl-shared-utils
3
+ Version: 0.1.0
4
+ Summary: Shared utilities for deepctl
5
+ Author-email: Deepgram <devrel@deepgram.com>
6
+ Maintainer-email: Deepgram <devrel@deepgram.com>
7
+ License-Expression: MIT
8
+ Keywords: deepgram,utilities,validation,helpers
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: pydantic>=2.0.0
18
+ Requires-Dist: click>=8.0.0
19
+ Requires-Dist: httpx>=0.24.0
20
+
21
+ # deepctl-shared-utils
22
+
23
+ Shared utilities for deepctl. This package is an internal component of deepctl and is not intended for standalone use.
@@ -0,0 +1,8 @@
1
+ deepctl_shared_utils/__init__.py,sha256=Q54OjCHV7ndxwJ3CBUBt257sgQL7AYogrzA7ZnkGaEs,275
2
+ deepctl_shared_utils/models.py,sha256=Sg9_Zh-xthUdxbESBy40bpM4nVl4zsyJOGF1sYaToXk,476
3
+ deepctl_shared_utils/py.typed,sha256=6f0ps5pRLTsVV1khUGtuRMcPzRHmQ_f3rM3_42XKqVU,60
4
+ deepctl_shared_utils/validation.py,sha256=uUGJpA6W4A1Eo8HvudpD7r0r8rTLlbxwD5yzMFGx2i8,14742
5
+ deepctl_shared_utils-0.1.0.dist-info/METADATA,sha256=U9tfbHO48u4yE-XhjrpCtqQ0uD6e-FgvsVFtVquaZC4,853
6
+ deepctl_shared_utils-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ deepctl_shared_utils-0.1.0.dist-info/top_level.txt,sha256=6lKjgFbNbA5Lniqel9HNhViYXKPoICnh3Q3_54Qm9Zk,21
8
+ deepctl_shared_utils-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 @@
1
+ deepctl_shared_utils