deepctl-core 0.2.19__tar.gz → 0.3.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.
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/PKG-INFO +1 -1
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/pyproject.toml +1 -1
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/auth.py +109 -217
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/config.py +0 -3
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core.egg-info/PKG-INFO +1 -1
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/README.md +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/setup.cfg +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/__init__.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/base_command.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/base_group_command.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/client.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/models.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/output.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/plugin_env.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/plugin_manager.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/py.typed +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/skill_bundle.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/skill_generator.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core/timing.py +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core.egg-info/SOURCES.txt +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core.egg-info/dependency_links.txt +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core.egg-info/requires.txt +0 -0
- {deepctl_core-0.2.19 → deepctl_core-0.3.0}/src/deepctl_core.egg-info/top_level.txt +0 -0
|
@@ -8,6 +8,7 @@ from urllib.parse import urlencode
|
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
import keyring
|
|
11
|
+
from keyring.errors import PasswordDeleteError
|
|
11
12
|
from pydantic import BaseModel
|
|
12
13
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
13
14
|
|
|
@@ -25,8 +26,6 @@ console = stderr_console
|
|
|
25
26
|
AUTH_BASE_URL = os.getenv("DEEPGRAM_CLI_BASE_URL", "https://id.dx.deepgram.com")
|
|
26
27
|
DEVICE_CODE_URL = f"{AUTH_BASE_URL}/device/code"
|
|
27
28
|
TOKEN_POLL_URL = f"{AUTH_BASE_URL}/device/token"
|
|
28
|
-
TOKEN_REFRESH_URL = f"{AUTH_BASE_URL}/token"
|
|
29
|
-
DG_CREDENTIALS_URL = f"{AUTH_BASE_URL}/credentials/token"
|
|
30
29
|
|
|
31
30
|
# Registered OIDC client ID for the CLI
|
|
32
31
|
CLIENT_ID = "deepgram-cli"
|
|
@@ -39,8 +38,9 @@ class DeviceCodeResponse(BaseModel):
|
|
|
39
38
|
"""Response from device code request."""
|
|
40
39
|
|
|
41
40
|
device_code: str
|
|
42
|
-
user_code: str | None = None
|
|
41
|
+
user_code: str | None = None
|
|
43
42
|
verification_uri: str
|
|
43
|
+
verification_uri_complete: str | None = None
|
|
44
44
|
expires_in: int
|
|
45
45
|
interval: int
|
|
46
46
|
|
|
@@ -50,15 +50,8 @@ class TokenResponse(BaseModel):
|
|
|
50
50
|
|
|
51
51
|
access_token: str
|
|
52
52
|
project_id: str
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expires_in: int | None = None
|
|
56
|
-
scope: str | None = None
|
|
57
|
-
|
|
58
|
-
@property
|
|
59
|
-
def api_key(self) -> str:
|
|
60
|
-
"""Get the API key from the response."""
|
|
61
|
-
return self.access_token
|
|
53
|
+
dg_token: str | None = None
|
|
54
|
+
dg_expires_in: int | None = None
|
|
62
55
|
|
|
63
56
|
|
|
64
57
|
class AuthenticationError(Exception):
|
|
@@ -113,19 +106,21 @@ class AuthManager:
|
|
|
113
106
|
"""
|
|
114
107
|
profile_name = profile_name or self.config.profile or "default"
|
|
115
108
|
|
|
116
|
-
# Check keyring for direct API key
|
|
109
|
+
# Check keyring for a direct API key.
|
|
117
110
|
has_api_key = False
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
expired, _ = self._credential_expiry(profile_name)
|
|
112
|
+
if not expired:
|
|
113
|
+
try:
|
|
114
|
+
has_api_key = bool(
|
|
115
|
+
keyring.get_password(KEYRING_SERVICE, f"api-key.{profile_name}")
|
|
116
|
+
)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
124
119
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
# If not in keyring, check config.
|
|
121
|
+
if not has_api_key:
|
|
122
|
+
profile = self.config.get_profile(profile_name)
|
|
123
|
+
has_api_key = bool(profile.api_key)
|
|
129
124
|
|
|
130
125
|
# Check for project ID in profile
|
|
131
126
|
profile = self.config.get_profile(profile_name)
|
|
@@ -159,15 +154,11 @@ class AuthManager:
|
|
|
159
154
|
return bool(os.getenv("DEEPGRAM_API_KEY") and os.getenv("DEEPGRAM_PROJECT_ID"))
|
|
160
155
|
|
|
161
156
|
def get_api_key(self, ignore_env: bool = False) -> str | None:
|
|
162
|
-
"""Get API key following precedence: explicit >
|
|
163
|
-
|
|
164
|
-
When the user logged in via device flow with the new JWT-based server,
|
|
165
|
-
this method transparently refreshes the JWT if expired and exchanges it
|
|
166
|
-
for a Deepgram API token (dg_token). All callers receive a usable
|
|
167
|
-
Deepgram token regardless of which auth path was used.
|
|
157
|
+
"""Get API key following precedence: explicit > profile > env.
|
|
168
158
|
|
|
169
|
-
|
|
170
|
-
|
|
159
|
+
Expired finite profile credentials are treated as absent, allowing an
|
|
160
|
+
explicit key or ``DEEPGRAM_API_KEY`` to continue to work. If no other
|
|
161
|
+
credential is available, raise an actionable login error.
|
|
171
162
|
|
|
172
163
|
Args:
|
|
173
164
|
ignore_env: If True, don't check environment variables
|
|
@@ -177,27 +168,24 @@ class AuthManager:
|
|
|
177
168
|
return self.explicit_api_key
|
|
178
169
|
|
|
179
170
|
profile_name = self.config.profile or "default"
|
|
171
|
+
current_profile = self.config.get_profile(profile_name)
|
|
180
172
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if api_key:
|
|
193
|
-
return api_key
|
|
194
|
-
except Exception:
|
|
195
|
-
pass
|
|
173
|
+
expired, expiry_error = self._credential_expiry(profile_name)
|
|
174
|
+
if not expired:
|
|
175
|
+
# Profile keyring entry, followed by its config-file fallback.
|
|
176
|
+
try:
|
|
177
|
+
api_key = keyring.get_password(
|
|
178
|
+
KEYRING_SERVICE, f"api-key.{profile_name}"
|
|
179
|
+
)
|
|
180
|
+
if api_key:
|
|
181
|
+
return api_key
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
196
184
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
if current_profile.api_key:
|
|
186
|
+
return current_profile.api_key
|
|
187
|
+
else:
|
|
188
|
+
self._delete_keyring_credential(f"api-key.{profile_name}")
|
|
201
189
|
|
|
202
190
|
# Environment variable — lowest priority.
|
|
203
191
|
if not ignore_env:
|
|
@@ -205,8 +193,43 @@ class AuthManager:
|
|
|
205
193
|
if api_key:
|
|
206
194
|
return api_key
|
|
207
195
|
|
|
196
|
+
if expiry_error:
|
|
197
|
+
raise AuthenticationError(expiry_error)
|
|
198
|
+
|
|
208
199
|
return None
|
|
209
200
|
|
|
201
|
+
def _credential_expiry(self, profile_name: str) -> tuple[bool, str | None]:
|
|
202
|
+
"""Return whether a finite profile credential is unusable and why."""
|
|
203
|
+
expires_at = self.config.get_profile(profile_name).dg_token_expires_at
|
|
204
|
+
if not isinstance(expires_at, str):
|
|
205
|
+
return False, None
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
parsed_expiry = datetime.fromisoformat(expires_at)
|
|
209
|
+
if parsed_expiry.tzinfo is None:
|
|
210
|
+
raise ValueError
|
|
211
|
+
except (TypeError, ValueError):
|
|
212
|
+
return (
|
|
213
|
+
True,
|
|
214
|
+
"Your Deepgram credential expiry is invalid. Run `dg login` again.",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if parsed_expiry <= datetime.now(timezone.utc):
|
|
218
|
+
return True, "Your Deepgram credential has expired. Run `dg login` again."
|
|
219
|
+
|
|
220
|
+
return False, None
|
|
221
|
+
|
|
222
|
+
def _delete_keyring_credential(self, keyring_key: str) -> None:
|
|
223
|
+
"""Remove a credential when present without masking keyring failures."""
|
|
224
|
+
try:
|
|
225
|
+
keyring.delete_password(KEYRING_SERVICE, keyring_key)
|
|
226
|
+
except PasswordDeleteError:
|
|
227
|
+
pass
|
|
228
|
+
except Exception as exc:
|
|
229
|
+
console.print(
|
|
230
|
+
f"[yellow]Warning:[/yellow] Could not remove legacy credential: {exc}"
|
|
231
|
+
)
|
|
232
|
+
|
|
210
233
|
def get_project_id(self, ignore_env: bool = False) -> str | None:
|
|
211
234
|
"""Get project ID following precedence: explicit > profile > env.
|
|
212
235
|
|
|
@@ -501,14 +524,9 @@ class AuthManager:
|
|
|
501
524
|
# Request device code from dx-id auth provider
|
|
502
525
|
device_response = self._request_device_code()
|
|
503
526
|
|
|
504
|
-
# Build verification URI with query parameters
|
|
505
|
-
query_params = {
|
|
506
|
-
"device_code": device_response.device_code,
|
|
507
|
-
"client_id": CLIENT_ID,
|
|
508
|
-
"hostname": hostname,
|
|
509
|
-
}
|
|
510
527
|
verification_uri = (
|
|
511
|
-
|
|
528
|
+
device_response.verification_uri_complete
|
|
529
|
+
or device_response.verification_uri
|
|
512
530
|
)
|
|
513
531
|
|
|
514
532
|
# Open browser immediately and start polling
|
|
@@ -516,6 +534,8 @@ class AuthManager:
|
|
|
516
534
|
"\n[bold]Hello from Deepgram![/bold] Opening browser to "
|
|
517
535
|
"complete login..."
|
|
518
536
|
)
|
|
537
|
+
if device_response.user_code:
|
|
538
|
+
console.print(f"[bold]Enter code:[/bold] {device_response.user_code}")
|
|
519
539
|
|
|
520
540
|
try:
|
|
521
541
|
webbrowser.open(verification_uri)
|
|
@@ -552,7 +572,7 @@ class AuthManager:
|
|
|
552
572
|
payload = {
|
|
553
573
|
"client_id": CLIENT_ID,
|
|
554
574
|
"hostname": hostname,
|
|
555
|
-
"scopes": "
|
|
575
|
+
"scopes": "openid profile email",
|
|
556
576
|
}
|
|
557
577
|
|
|
558
578
|
try:
|
|
@@ -628,62 +648,28 @@ class AuthManager:
|
|
|
628
648
|
def _store_token(self, token_response: TokenResponse) -> None:
|
|
629
649
|
"""Store authentication token.
|
|
630
650
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
- Legacy flow: access_token is a Deepgram API key directly.
|
|
635
|
-
Stored under api-key.{profile} as before.
|
|
651
|
+
Stores a direct finite Deepgram credential from dx-id. The credential
|
|
652
|
+
expires independently of the OIDC access token and requires a new
|
|
653
|
+
device authorization when it expires.
|
|
636
654
|
"""
|
|
637
655
|
profile_name = self.config.profile or "default"
|
|
638
656
|
|
|
639
|
-
if token_response.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
657
|
+
if token_response.dg_token:
|
|
658
|
+
api_key = token_response.dg_token
|
|
659
|
+
expires_in = token_response.dg_expires_in or 0
|
|
660
|
+
if expires_in <= 0:
|
|
661
|
+
raise AuthenticationError(
|
|
662
|
+
"Deepgram sign-in did not provide a finite credential expiry"
|
|
663
|
+
)
|
|
644
664
|
expires_at = (
|
|
645
665
|
datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
|
646
666
|
).isoformat()
|
|
647
667
|
|
|
648
|
-
try:
|
|
649
|
-
# Clear any stale direct API key so get_api_key() doesn't
|
|
650
|
-
# return a cached dg_token from a previous session.
|
|
651
|
-
keyring.delete_password(KEYRING_SERVICE, f"api-key.{profile_name}")
|
|
652
|
-
except Exception:
|
|
653
|
-
pass
|
|
654
|
-
|
|
655
|
-
try:
|
|
656
|
-
keyring.set_password(KEYRING_SERVICE, f"jwt.{profile_name}", jwt)
|
|
657
|
-
keyring.set_password(
|
|
658
|
-
KEYRING_SERVICE, f"refresh-token.{profile_name}", refresh_token
|
|
659
|
-
)
|
|
660
|
-
console.print(
|
|
661
|
-
"[green]✓[/green] Session stored securely in system keyring"
|
|
662
|
-
)
|
|
663
|
-
except Exception as e:
|
|
664
|
-
console.print(
|
|
665
|
-
f"[yellow]Warning:[/yellow] Could not store in keyring: {e}"
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
# Store expiry timestamps and project ID in config (non-sensitive).
|
|
669
|
-
profile = self.config.get_profile(profile_name)
|
|
670
|
-
profile.jwt_expires_at = expires_at
|
|
671
|
-
profile.project_id = token_response.project_id
|
|
672
|
-
self.config.save()
|
|
673
|
-
|
|
674
|
-
else:
|
|
675
|
-
# Legacy flow: access_token is the Deepgram API key directly.
|
|
676
|
-
api_key = token_response.access_token
|
|
677
|
-
project_id = token_response.project_id
|
|
678
668
|
keyring_available = False
|
|
679
|
-
|
|
680
669
|
try:
|
|
681
670
|
keyring.set_password(
|
|
682
671
|
KEYRING_SERVICE, f"api-key.{profile_name}", api_key
|
|
683
672
|
)
|
|
684
|
-
console.print(
|
|
685
|
-
"[green]✓[/green] API key stored securely in system keyring"
|
|
686
|
-
)
|
|
687
673
|
keyring_available = True
|
|
688
674
|
except Exception as e:
|
|
689
675
|
console.print(
|
|
@@ -691,11 +677,31 @@ class AuthManager:
|
|
|
691
677
|
)
|
|
692
678
|
console.print("API key will be stored in config file instead")
|
|
693
679
|
|
|
680
|
+
for keyring_key in (f"jwt.{profile_name}", f"refresh-token.{profile_name}"):
|
|
681
|
+
self._delete_keyring_credential(keyring_key)
|
|
682
|
+
|
|
694
683
|
self.config.create_profile(
|
|
695
684
|
profile_name,
|
|
696
685
|
api_key=api_key if not keyring_available else None,
|
|
697
|
-
project_id=project_id,
|
|
686
|
+
project_id=token_response.project_id,
|
|
687
|
+
base_url=self.config.get_profile(profile_name).base_url,
|
|
698
688
|
)
|
|
689
|
+
profile = self.config.get_profile(profile_name)
|
|
690
|
+
profile.dg_token_expires_at = expires_at
|
|
691
|
+
self.config.save()
|
|
692
|
+
if keyring_available:
|
|
693
|
+
console.print(
|
|
694
|
+
"[green]✓[/green] API key stored securely in system keyring"
|
|
695
|
+
)
|
|
696
|
+
else:
|
|
697
|
+
console.print(
|
|
698
|
+
"[yellow]Warning:[/yellow] API key stored in the config file fallback"
|
|
699
|
+
)
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
raise AuthenticationError(
|
|
703
|
+
"Deepgram sign-in did not return a finite credential. Run `dg update` and `dg login` again."
|
|
704
|
+
)
|
|
699
705
|
|
|
700
706
|
def logout(self, keep_config: bool = False) -> None:
|
|
701
707
|
"""Logout user and clear credentials.
|
|
@@ -769,120 +775,6 @@ class AuthManager:
|
|
|
769
775
|
current_profile=self.config.profile or self.config._config.default_profile,
|
|
770
776
|
)
|
|
771
777
|
|
|
772
|
-
def _get_dg_token_via_jwt(self, jwt: str, profile_name: str) -> str:
|
|
773
|
-
"""Return a valid Deepgram API token, refreshing as needed.
|
|
774
|
-
|
|
775
|
-
Checks the cached dg_token first; refreshes the JWT if expired; then
|
|
776
|
-
exchanges the JWT for a fresh dg_token when the cached one is stale.
|
|
777
|
-
"""
|
|
778
|
-
profile = self.config.get_profile(profile_name)
|
|
779
|
-
now = datetime.now(timezone.utc)
|
|
780
|
-
|
|
781
|
-
# Return cached dg_token if it is still valid.
|
|
782
|
-
try:
|
|
783
|
-
dg_token = keyring.get_password(KEYRING_SERVICE, f"api-key.{profile_name}")
|
|
784
|
-
if dg_token and profile.dg_token_expires_at:
|
|
785
|
-
expires = datetime.fromisoformat(profile.dg_token_expires_at)
|
|
786
|
-
if now < expires:
|
|
787
|
-
return dg_token
|
|
788
|
-
except Exception:
|
|
789
|
-
pass
|
|
790
|
-
|
|
791
|
-
# Refresh the JWT if it has expired.
|
|
792
|
-
current_jwt = jwt
|
|
793
|
-
if profile.jwt_expires_at:
|
|
794
|
-
jwt_expires = datetime.fromisoformat(profile.jwt_expires_at)
|
|
795
|
-
if now >= jwt_expires:
|
|
796
|
-
current_jwt = self._refresh_jwt(profile_name)
|
|
797
|
-
|
|
798
|
-
return self._exchange_for_dg_token(current_jwt, profile_name)
|
|
799
|
-
|
|
800
|
-
def _refresh_jwt(self, profile_name: str) -> str:
|
|
801
|
-
"""Exchange a refresh token for a new JWT."""
|
|
802
|
-
try:
|
|
803
|
-
refresh_token = keyring.get_password(
|
|
804
|
-
KEYRING_SERVICE, f"refresh-token.{profile_name}"
|
|
805
|
-
)
|
|
806
|
-
except Exception:
|
|
807
|
-
refresh_token = None
|
|
808
|
-
|
|
809
|
-
if not refresh_token:
|
|
810
|
-
raise AuthenticationError(
|
|
811
|
-
"Session expired and no refresh token is available. "
|
|
812
|
-
"Please run 'dg login' again."
|
|
813
|
-
)
|
|
814
|
-
|
|
815
|
-
try:
|
|
816
|
-
response = self.client.post(
|
|
817
|
-
TOKEN_REFRESH_URL,
|
|
818
|
-
json={"grant_type": "refresh_token", "refresh_token": refresh_token},
|
|
819
|
-
)
|
|
820
|
-
except httpx.RequestError as e:
|
|
821
|
-
raise AuthenticationError(f"Network error during token refresh: {e}")
|
|
822
|
-
|
|
823
|
-
if response.status_code not in (200, 201):
|
|
824
|
-
raise AuthenticationError(
|
|
825
|
-
f"Token refresh failed (HTTP {response.status_code}). "
|
|
826
|
-
"Please run 'dg login' again."
|
|
827
|
-
)
|
|
828
|
-
|
|
829
|
-
data = response.json()
|
|
830
|
-
new_jwt: str = data["access_token"]
|
|
831
|
-
new_refresh: str = data.get("refresh_token", refresh_token)
|
|
832
|
-
expires_in: int = data.get("expires_in", 900)
|
|
833
|
-
expires_at = (
|
|
834
|
-
datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
|
835
|
-
).isoformat()
|
|
836
|
-
|
|
837
|
-
try:
|
|
838
|
-
keyring.set_password(KEYRING_SERVICE, f"jwt.{profile_name}", new_jwt)
|
|
839
|
-
if new_refresh != refresh_token:
|
|
840
|
-
keyring.set_password(
|
|
841
|
-
KEYRING_SERVICE, f"refresh-token.{profile_name}", new_refresh
|
|
842
|
-
)
|
|
843
|
-
except Exception:
|
|
844
|
-
pass
|
|
845
|
-
|
|
846
|
-
profile = self.config.get_profile(profile_name)
|
|
847
|
-
profile.jwt_expires_at = expires_at
|
|
848
|
-
self.config.save()
|
|
849
|
-
|
|
850
|
-
return new_jwt
|
|
851
|
-
|
|
852
|
-
def _exchange_for_dg_token(self, jwt: str, profile_name: str) -> str:
|
|
853
|
-
"""Exchange a JWT for a Deepgram API token (dg_token)."""
|
|
854
|
-
try:
|
|
855
|
-
response = self.client.post(
|
|
856
|
-
DG_CREDENTIALS_URL,
|
|
857
|
-
headers={"Authorization": f"Bearer {jwt}"},
|
|
858
|
-
)
|
|
859
|
-
except httpx.RequestError as e:
|
|
860
|
-
raise AuthenticationError(f"Network error during credentials exchange: {e}")
|
|
861
|
-
|
|
862
|
-
if response.status_code not in (200, 201):
|
|
863
|
-
raise AuthenticationError(
|
|
864
|
-
f"Credentials exchange failed (HTTP {response.status_code}). "
|
|
865
|
-
"Please run 'dg login' again."
|
|
866
|
-
)
|
|
867
|
-
|
|
868
|
-
data = response.json()
|
|
869
|
-
dg_token: str = data["dg_token"]
|
|
870
|
-
expires_in: int = data.get("expires_in", 86400)
|
|
871
|
-
expires_at = (
|
|
872
|
-
datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
|
873
|
-
).isoformat()
|
|
874
|
-
|
|
875
|
-
try:
|
|
876
|
-
keyring.set_password(KEYRING_SERVICE, f"api-key.{profile_name}", dg_token)
|
|
877
|
-
except Exception:
|
|
878
|
-
pass # Non-fatal: will re-exchange next time
|
|
879
|
-
|
|
880
|
-
profile = self.config.get_profile(profile_name)
|
|
881
|
-
profile.dg_token_expires_at = expires_at
|
|
882
|
-
self.config.save()
|
|
883
|
-
|
|
884
|
-
return dg_token
|
|
885
|
-
|
|
886
778
|
def __del__(self) -> None:
|
|
887
779
|
"""Cleanup HTTP client."""
|
|
888
780
|
if hasattr(self, "client"):
|
|
@@ -15,9 +15,6 @@ class ProfileConfig(BaseModel):
|
|
|
15
15
|
api_key: str | None = None
|
|
16
16
|
project_id: str | None = None
|
|
17
17
|
base_url: str = "https://api.deepgram.com"
|
|
18
|
-
# JWT-based auth (device flow with new server): expiry timestamps stored
|
|
19
|
-
# here (non-sensitive); the tokens themselves live in the keyring.
|
|
20
|
-
jwt_expires_at: str | None = None
|
|
21
18
|
dg_token_expires_at: str | None = None
|
|
22
19
|
|
|
23
20
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|