deepctl-cmd-login 0.1.0__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.
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepctl-cmd-login
3
+ Version: 0.1.0
4
+ Summary: Login 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,login,authentication
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: click>=8.0.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: deepgram-sdk>=3.0.0
21
+ Requires-Dist: pydantic>=2.0.0
22
+
23
+ # deepctl-cmd-login
24
+
25
+ Login command for deepctl. This package is an internal component of deepctl and is not intended for standalone use.
@@ -0,0 +1,3 @@
1
+ # deepctl-cmd-login
2
+
3
+ Login command for deepctl. This package is an internal component of deepctl and is not intended for standalone use.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "deepctl-cmd-login"
7
+ version = "0.1.0"
8
+ description = "Login command for deepctl"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "Deepgram", email = "devrel@deepgram.com" }]
12
+ maintainers = [{ name = "Deepgram", email = "devrel@deepgram.com" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+ keywords = ["deepgram", "cli", "login", "authentication"]
22
+ requires-python = ">=3.10"
23
+ dependencies = [
24
+ "deepctl-core>=0.1.0",
25
+ "click>=8.0.0",
26
+ "rich>=13.0.0",
27
+ "deepgram-sdk>=3.0.0",
28
+ "pydantic>=2.0.0",
29
+ ]
30
+
31
+ [project.scripts]
32
+ # Internal commands not meant to be called directly
33
+
34
+ [project.entry-points."deepctl.commands"]
35
+ login = "deepctl_cmd_login.command:LoginCommand"
36
+ logout = "deepctl_cmd_login.command:LogoutCommand"
37
+ profiles = "deepctl_cmd_login.command:ProfilesCommand"
38
+
39
+ [tool.setuptools]
40
+ package-dir = { "" = "src" }
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+ include = ["deepctl_cmd_login*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Login command package for deepctl."""
2
+
3
+ from .command import LoginCommand
4
+ from .models import LoginResult, LogoutResult
5
+
6
+ __all__ = ["LoginCommand", "LoginResult", "LogoutResult"]
@@ -0,0 +1,495 @@
1
+ """Login command for deepctl."""
2
+
3
+ from typing import Any
4
+
5
+ from deepctl_core import (
6
+ AuthenticationError,
7
+ AuthManager,
8
+ BaseCommand,
9
+ Config,
10
+ DeepgramClient,
11
+ ProfileInfo,
12
+ ProfilesResult,
13
+ )
14
+ from rich.console import Console
15
+
16
+ from .models import LoginResult, LogoutResult
17
+
18
+ console = Console()
19
+
20
+
21
+ class LoginCommand(BaseCommand):
22
+ """Login command for authenticating with Deepgram."""
23
+
24
+ name = "login"
25
+ help = "Log in to Deepgram with browser-based authentication or API key"
26
+ short_help = "Log in to Deepgram"
27
+
28
+ # Login doesn't require existing auth
29
+ requires_auth = False
30
+ requires_project = False
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": ["--api-key", "-k"],
38
+ "help": "Configure the CLI with your Deepgram API key",
39
+ "type": str,
40
+ "required": False,
41
+ "is_option": True,
42
+ },
43
+ {
44
+ "names": ["--project-id", "-p"],
45
+ "help": "Configure the CLI with your Deepgram project ID",
46
+ "type": str,
47
+ "required": False,
48
+ "is_option": True,
49
+ },
50
+ {
51
+ "names": ["--force-write", "-f"],
52
+ "help": (
53
+ "Don't prompt for confirmation when providing "
54
+ "credentials"
55
+ ),
56
+ "is_flag": True,
57
+ "is_option": True,
58
+ },
59
+ {
60
+ "names": ["--profile"],
61
+ "help": "Profile name to use for storing credentials",
62
+ "type": str,
63
+ "required": False,
64
+ "is_option": True,
65
+ },
66
+ ]
67
+
68
+ def handle(
69
+ self,
70
+ config: Config,
71
+ auth_manager: AuthManager,
72
+ client: DeepgramClient,
73
+ **kwargs: Any,
74
+ ) -> Any:
75
+ """Handle login command."""
76
+ api_key = kwargs.get("api_key")
77
+ project_id = kwargs.get("project_id")
78
+ force_write = kwargs.get("force_write", False)
79
+ profile = kwargs.get("profile")
80
+
81
+ # Set profile if provided
82
+ if profile:
83
+ config.profile = profile
84
+
85
+ # Check if user is already logged in
86
+ if auth_manager.is_authenticated() and not force_write:
87
+ console.print(
88
+ f"[yellow]Already logged in to profile:[/yellow] "
89
+ f"{config.profile or 'default'}"
90
+ )
91
+
92
+ if not self.confirm("Do you want to login again?", default=False):
93
+ return LoginResult(
94
+ status="cancelled",
95
+ message="Login cancelled by user",
96
+ profile=config.profile or "default",
97
+ api_key_masked=None,
98
+ )
99
+
100
+ # Determine authentication method
101
+ if api_key:
102
+ return self._cli_auth(
103
+ config, auth_manager, str(api_key), project_id, force_write
104
+ )
105
+ else:
106
+ return self._web_auth(config, auth_manager, force_write)
107
+
108
+ def _cli_auth(
109
+ self,
110
+ config: Config,
111
+ auth_manager: AuthManager,
112
+ api_key: str,
113
+ project_id: str | None,
114
+ force_write: bool,
115
+ ) -> LoginResult:
116
+ """Handle CLI authentication with API key."""
117
+ console.print("[blue]Configuring CLI with API key...[/blue]")
118
+
119
+ # Validate API key format
120
+ if not api_key.startswith(("sk-", "pk-")):
121
+ console.print(
122
+ "[yellow]Warning:[/yellow] API key format doesn't match "
123
+ "expected pattern"
124
+ )
125
+ if not force_write and not self.confirm(
126
+ "Continue anyway?", default=False
127
+ ):
128
+ return LoginResult(
129
+ status="cancelled",
130
+ message="Login cancelled by user",
131
+ profile=config.profile or "default",
132
+ api_key_masked=None,
133
+ )
134
+
135
+ # Validate project ID is provided with API key
136
+ if not project_id:
137
+ console.print("[yellow]Warning:[/yellow] Project ID not provided")
138
+ console.print(
139
+ "You can set it later with: "
140
+ "deepctl login --project-id <project_id>"
141
+ )
142
+ console.print("Or use environment variable: DEEPGRAM_PROJECT_ID")
143
+
144
+ # Check if config file exists and prompt for overwrite
145
+ if not force_write:
146
+ if config.config_path.exists():
147
+ console.print(
148
+ f"[yellow]Configuration file already exists:[/yellow] "
149
+ f"{config.config_path}"
150
+ )
151
+ if not self.confirm(
152
+ "Overwrite existing configuration?", default=False
153
+ ):
154
+ return LoginResult(
155
+ status="cancelled",
156
+ message="Login cancelled by user",
157
+ profile=config.profile or "default",
158
+ api_key_masked=None,
159
+ )
160
+ else:
161
+ if not self.confirm(
162
+ "Do you want to write these credentials to config?",
163
+ default=True,
164
+ ):
165
+ return LoginResult(
166
+ status="cancelled",
167
+ message="Login cancelled by user",
168
+ profile=config.profile or "default",
169
+ api_key_masked=None,
170
+ )
171
+
172
+ try:
173
+ # Store credentials (verification happens inside
174
+ # login_with_api_key)
175
+ auth_manager.login_with_api_key(
176
+ api_key, project_id or "", force_write
177
+ )
178
+
179
+ profile_name = config.profile or "default"
180
+ return LoginResult(
181
+ status="success",
182
+ message="Successfully logged in with API key",
183
+ profile=profile_name,
184
+ api_key_masked=f"****{api_key[-4:]}",
185
+ project_id=project_id,
186
+ config_path=str(config.config_path),
187
+ )
188
+
189
+ except AuthenticationError as e:
190
+ console.print(f"[red]Authentication failed:[/red] {e}")
191
+ return LoginResult(
192
+ status="error",
193
+ message=str(e),
194
+ profile=config.profile or "default",
195
+ api_key_masked=None,
196
+ )
197
+
198
+ except Exception as e:
199
+ console.print(f"[red]Error during CLI authentication:[/red] {e}")
200
+ return LoginResult(
201
+ status="error",
202
+ message=str(e),
203
+ profile=config.profile or "default",
204
+ api_key_masked=None,
205
+ )
206
+
207
+ def _web_auth(
208
+ self, config: Config, auth_manager: AuthManager, force_write: bool
209
+ ) -> LoginResult:
210
+ """Handle web authentication with device flow."""
211
+ console.print("[blue]Starting web authentication...[/blue]")
212
+
213
+ # Check if config file exists and prompt for overwrite
214
+ if not force_write and config.config_path.exists():
215
+ console.print(
216
+ f"[yellow]Configuration file already exists:[/yellow] "
217
+ f"{config.config_path}"
218
+ )
219
+ if not self.confirm(
220
+ "Overwrite existing configuration?", default=False
221
+ ):
222
+ return LoginResult(
223
+ status="cancelled",
224
+ message="Login cancelled by user",
225
+ profile=config.profile or "default",
226
+ api_key_masked=None,
227
+ )
228
+
229
+ try:
230
+ # Start device flow
231
+ auth_manager.login_with_device_flow()
232
+
233
+ profile_name = config.profile or "default"
234
+
235
+ # Retrieve the stored credentials
236
+ api_key = auth_manager.get_api_key()
237
+ project_id = auth_manager.get_project_id()
238
+
239
+ # Mask the API key for display
240
+ api_key_masked = None
241
+ if api_key:
242
+ api_key_masked = f"****{api_key[-4:]}"
243
+
244
+ return LoginResult(
245
+ status="success",
246
+ message="Successfully logged in via web authentication",
247
+ profile=profile_name,
248
+ api_key_masked=api_key_masked,
249
+ project_id=project_id,
250
+ config_path=str(config.config_path),
251
+ )
252
+
253
+ except AuthenticationError as e:
254
+ console.print(f"[red]Authentication failed:[/red] {e}")
255
+ return LoginResult(
256
+ status="error",
257
+ message=str(e),
258
+ profile=config.profile or "default",
259
+ api_key_masked=None,
260
+ )
261
+
262
+ except KeyboardInterrupt:
263
+ console.print(
264
+ "\n[yellow]Authentication cancelled by user[/yellow]"
265
+ )
266
+ return LoginResult(
267
+ status="cancelled",
268
+ message="Login cancelled by user",
269
+ profile=config.profile or "default",
270
+ api_key_masked=None,
271
+ )
272
+
273
+ except Exception as e:
274
+ console.print(f"[red]Error during web authentication:[/red] {e}")
275
+ return LoginResult(
276
+ status="error",
277
+ message=str(e),
278
+ profile=config.profile or "default",
279
+ api_key_masked=None,
280
+ )
281
+
282
+
283
+ class LogoutCommand(BaseCommand):
284
+ """Logout command for clearing authentication."""
285
+
286
+ name = "logout"
287
+ help = "Log out and clear stored credentials"
288
+ short_help = "Log out of Deepgram"
289
+
290
+ # Logout doesn't require existing auth
291
+ requires_auth = False
292
+ requires_project = False
293
+ ci_friendly = True
294
+
295
+ def get_arguments(self) -> list[dict[str, Any]]:
296
+ """Get command arguments and options."""
297
+ return [
298
+ {
299
+ "names": ["--profile"],
300
+ "help": "Profile to logout from (default: current profile)",
301
+ "type": str,
302
+ "required": False,
303
+ "is_option": True,
304
+ },
305
+ {
306
+ "names": ["--all"],
307
+ "help": "Logout from all profiles",
308
+ "is_flag": True,
309
+ "is_option": True,
310
+ },
311
+ ]
312
+
313
+ def handle(
314
+ self,
315
+ config: Config,
316
+ auth_manager: AuthManager,
317
+ client: DeepgramClient,
318
+ **kwargs: Any,
319
+ ) -> Any:
320
+ """Handle logout command."""
321
+ profile = kwargs.get("profile")
322
+ logout_all = kwargs.get("all", False)
323
+
324
+ try:
325
+ if logout_all:
326
+ # Logout from all profiles
327
+ profiles = config.list_profiles()
328
+ for profile_name in profiles:
329
+ config.profile = profile_name
330
+ auth_manager_for_profile = AuthManager(config)
331
+ auth_manager_for_profile.logout()
332
+
333
+ console.print(
334
+ f"[green]✓[/green] Successfully logged out from all "
335
+ f"profiles ({len(profiles)} profiles)"
336
+ )
337
+ return LogoutResult(
338
+ status="success",
339
+ message="Logged out from all profiles",
340
+ profiles_count=len(profiles),
341
+ )
342
+
343
+ else:
344
+ # Logout from specific profile
345
+ if profile:
346
+ config.profile = profile
347
+
348
+ if not auth_manager.is_authenticated():
349
+ console.print("[yellow]Not currently logged in[/yellow]")
350
+ return LogoutResult(
351
+ status="info", message="Not currently logged in"
352
+ )
353
+
354
+ auth_manager.logout()
355
+
356
+ profile_name = config.profile or "default"
357
+ return LogoutResult(
358
+ status="success",
359
+ message=(
360
+ f"Successfully logged out from profile: {profile_name}"
361
+ ),
362
+ profile=profile_name,
363
+ )
364
+
365
+ except Exception as e:
366
+ console.print(f"[red]Error during logout:[/red] {e}")
367
+ return LogoutResult(status="error", message=str(e))
368
+
369
+
370
+ class ProfilesCommand(BaseCommand):
371
+ """Command to manage authentication profiles."""
372
+
373
+ name = "profiles"
374
+ help = "Manage authentication profiles"
375
+ short_help = "Manage profiles"
376
+
377
+ # Profiles command doesn't require auth
378
+ requires_auth = False
379
+ requires_project = False
380
+ ci_friendly = True
381
+
382
+ def get_arguments(self) -> list[dict[str, Any]]:
383
+ """Get command arguments and options."""
384
+ return [
385
+ {
386
+ "names": ["--list", "-l"],
387
+ "help": "List all profiles",
388
+ "is_flag": True,
389
+ "is_option": True,
390
+ },
391
+ {
392
+ "names": ["--current"],
393
+ "help": "Show current profile",
394
+ "is_flag": True,
395
+ "is_option": True,
396
+ },
397
+ {
398
+ "names": ["--switch"],
399
+ "help": "Switch to a different profile",
400
+ "type": str,
401
+ "required": False,
402
+ "is_option": True,
403
+ },
404
+ ]
405
+
406
+ def handle(
407
+ self,
408
+ config: Config,
409
+ auth_manager: AuthManager,
410
+ client: DeepgramClient,
411
+ **kwargs: Any,
412
+ ) -> Any:
413
+ """Handle profiles command."""
414
+ list_profiles = kwargs.get("list", False)
415
+ show_current = kwargs.get("current", False)
416
+ switch_profile = kwargs.get("switch")
417
+
418
+ if list_profiles:
419
+ profiles_result = auth_manager.list_profiles()
420
+ profiles = profiles_result.profiles
421
+ if not profiles:
422
+ console.print("[yellow]No profiles found[/yellow]")
423
+ return ProfilesResult(
424
+ status="info", message="No profiles found", profiles={}
425
+ )
426
+
427
+ console.print("[blue]Available profiles:[/blue]")
428
+ for name, info in profiles.items():
429
+ current_marker = (
430
+ " (current)"
431
+ if name == (config.profile or "default")
432
+ else ""
433
+ )
434
+ console.print(f" • {name}{current_marker}")
435
+ console.print(f" API Key: {info.api_key or 'Not set'}")
436
+ console.print(
437
+ f" Project ID: {info.project_id or 'Not set'}"
438
+ )
439
+ console.print(f" Base URL: {info.base_url}")
440
+ console.print()
441
+ return profiles_result
442
+
443
+ elif show_current:
444
+ current_profile = config.profile or "default"
445
+ profile_info = auth_manager.list_profiles().profiles.get(
446
+ current_profile,
447
+ ProfileInfo(api_key=None, project_id=None, base_url=""),
448
+ )
449
+
450
+ console.print(f"[blue]Current profile:[/blue] {current_profile}")
451
+ console.print(
452
+ f"[dim]API Key:[/dim] {profile_info.api_key or 'Not set'}"
453
+ )
454
+ console.print(
455
+ f"[dim]Project ID:[/dim] "
456
+ f"{profile_info.project_id or 'Not set'}"
457
+ )
458
+ console.print(
459
+ f"[dim]Base URL:[/dim] {profile_info.base_url or 'Not set'}"
460
+ )
461
+
462
+ return ProfilesResult(
463
+ status="success",
464
+ current_profile=current_profile,
465
+ profiles={current_profile: profile_info},
466
+ )
467
+
468
+ elif switch_profile:
469
+ profile_names = config.list_profiles()
470
+
471
+ if switch_profile not in profile_names:
472
+ console.print(
473
+ f"[red]Profile '{switch_profile}' not found[/red]"
474
+ )
475
+ return ProfilesResult(
476
+ status="error",
477
+ message=f"Profile '{switch_profile}' not found",
478
+ )
479
+
480
+ # Update default profile in config
481
+ config._config.default_profile = switch_profile
482
+ config.save()
483
+
484
+ console.print(
485
+ f"[green]✓[/green] Switched to profile: {switch_profile}"
486
+ )
487
+ return ProfilesResult(
488
+ status="success",
489
+ message=f"Switched to profile: {switch_profile}",
490
+ current_profile=switch_profile,
491
+ )
492
+
493
+ else:
494
+ # Default behavior - show current profile
495
+ return self.handle(config, auth_manager, client, current=True)
@@ -0,0 +1,22 @@
1
+ """Models for login command."""
2
+
3
+ from deepctl_core import BaseResult
4
+ from pydantic import Field
5
+
6
+
7
+ class LoginResult(BaseResult):
8
+ """Return structure for `deepctl login` command."""
9
+
10
+ profile: str
11
+ api_key_masked: str | None = Field(
12
+ None, description="Obfuscated key for display – e.g. ****abcd"
13
+ )
14
+ project_id: str | None = None
15
+ config_path: str | None = None
16
+
17
+
18
+ class LogoutResult(BaseResult):
19
+ """Return structure for logout command."""
20
+
21
+ profile: str | None = None
22
+ profiles_count: int | None = None # when --all is used
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepctl-cmd-login
3
+ Version: 0.1.0
4
+ Summary: Login 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,login,authentication
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: click>=8.0.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Requires-Dist: deepgram-sdk>=3.0.0
21
+ Requires-Dist: pydantic>=2.0.0
22
+
23
+ # deepctl-cmd-login
24
+
25
+ Login command for deepctl. This package is an internal component of deepctl and is not intended for standalone use.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/deepctl_cmd_login/__init__.py
4
+ src/deepctl_cmd_login/command.py
5
+ src/deepctl_cmd_login/models.py
6
+ src/deepctl_cmd_login.egg-info/PKG-INFO
7
+ src/deepctl_cmd_login.egg-info/SOURCES.txt
8
+ src/deepctl_cmd_login.egg-info/dependency_links.txt
9
+ src/deepctl_cmd_login.egg-info/entry_points.txt
10
+ src/deepctl_cmd_login.egg-info/requires.txt
11
+ src/deepctl_cmd_login.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ [deepctl.commands]
2
+ login = deepctl_cmd_login.command:LoginCommand
3
+ logout = deepctl_cmd_login.command:LogoutCommand
4
+ profiles = deepctl_cmd_login.command:ProfilesCommand
@@ -0,0 +1,5 @@
1
+ deepctl-core>=0.1.0
2
+ click>=8.0.0
3
+ rich>=13.0.0
4
+ deepgram-sdk>=3.0.0
5
+ pydantic>=2.0.0
@@ -0,0 +1 @@
1
+ deepctl_cmd_login