tokentoss 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.
- tokentoss/__init__.py +80 -0
- tokentoss/_logging.py +42 -0
- tokentoss/_telemetry.py +13 -0
- tokentoss/auth_manager.py +492 -0
- tokentoss/client.py +250 -0
- tokentoss/configure_widget.py +253 -0
- tokentoss/exceptions.py +56 -0
- tokentoss/setup.py +197 -0
- tokentoss/storage.py +195 -0
- tokentoss/widget.py +786 -0
- tokentoss-0.1.0.dist-info/METADATA +147 -0
- tokentoss-0.1.0.dist-info/RECORD +14 -0
- tokentoss-0.1.0.dist-info/WHEEL +4 -0
- tokentoss-0.1.0.dist-info/licenses/LICENSE +21 -0
tokentoss/client.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""IAP-authenticated HTTP client.
|
|
2
|
+
|
|
3
|
+
Provides IAPClient which automatically handles Google ID token
|
|
4
|
+
injection for requests to IAP-protected services.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from google.auth.transport.requests import Request
|
|
14
|
+
|
|
15
|
+
from .exceptions import NoCredentialsError
|
|
16
|
+
from .storage import FileStorage
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .auth_manager import AuthManager
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IAPClient:
|
|
23
|
+
"""HTTP client that adds IAP authentication tokens automatically.
|
|
24
|
+
|
|
25
|
+
Discovers credentials via a fallback chain:
|
|
26
|
+
1. Explicit AuthManager (passed to constructor)
|
|
27
|
+
2. Module-level tokentoss.CREDENTIALS (set by AuthManager on login)
|
|
28
|
+
3. Token file (TOKENTOSS_TOKEN_FILE env var or default platformdirs path)
|
|
29
|
+
|
|
30
|
+
Automatically retries once on 401 by refreshing the token.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
client = IAPClient(base_url="https://my-iap-service.run.app")
|
|
34
|
+
data = client.get_json("/api/data")
|
|
35
|
+
|
|
36
|
+
Or as a context manager:
|
|
37
|
+
with IAPClient(base_url="https://my-service.run.app") as client:
|
|
38
|
+
data = client.get_json("/api/data")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
base_url: str | None = None,
|
|
44
|
+
auth_manager: AuthManager | None = None,
|
|
45
|
+
timeout: int = 30,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize IAPClient.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_url: Optional base URL for relative paths (e.g. "https://my-service.run.app").
|
|
51
|
+
auth_manager: Optional AuthManager for credential discovery.
|
|
52
|
+
If not provided, falls back to module-level CREDENTIALS or token file.
|
|
53
|
+
timeout: Request timeout in seconds. Default 30.
|
|
54
|
+
"""
|
|
55
|
+
self.base_url = base_url.rstrip("/") if base_url else None
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
self._auth_manager = auth_manager
|
|
58
|
+
self._session = requests.Session()
|
|
59
|
+
self._fallback_storage: FileStorage | None = None
|
|
60
|
+
|
|
61
|
+
def _get_fallback_storage(self) -> FileStorage:
|
|
62
|
+
"""Get or create cached FileStorage for token discovery."""
|
|
63
|
+
if self._fallback_storage is None:
|
|
64
|
+
token_path = os.environ.get("TOKENTOSS_TOKEN_FILE")
|
|
65
|
+
self._fallback_storage = FileStorage(path=token_path)
|
|
66
|
+
return self._fallback_storage
|
|
67
|
+
|
|
68
|
+
def _get_id_token(self, force_refresh: bool = False) -> str:
|
|
69
|
+
"""Find and return a valid ID token.
|
|
70
|
+
|
|
71
|
+
Discovery chain:
|
|
72
|
+
1. Explicit auth_manager passed in constructor
|
|
73
|
+
2. Module-level tokentoss.CREDENTIALS variable
|
|
74
|
+
3. Token file (TOKENTOSS_TOKEN_FILE env var or default location)
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
force_refresh: Force a token refresh before returning.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A valid ID token string.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
NoCredentialsError: If no valid credentials found anywhere.
|
|
84
|
+
"""
|
|
85
|
+
# 1. Explicit AuthManager
|
|
86
|
+
token = self._try_auth_manager(force_refresh)
|
|
87
|
+
if token:
|
|
88
|
+
return token
|
|
89
|
+
|
|
90
|
+
# 2. Module-level credentials
|
|
91
|
+
token = self._try_module_credentials(force_refresh)
|
|
92
|
+
if token:
|
|
93
|
+
return token
|
|
94
|
+
|
|
95
|
+
# 3. Token file (env var or default path)
|
|
96
|
+
token = self._try_storage()
|
|
97
|
+
if token:
|
|
98
|
+
return token
|
|
99
|
+
|
|
100
|
+
raise NoCredentialsError(
|
|
101
|
+
"No valid credentials found. Use GoogleAuthWidget to authenticate."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _try_auth_manager(self, force_refresh: bool) -> str | None:
|
|
105
|
+
"""Try to get ID token from explicit AuthManager."""
|
|
106
|
+
if self._auth_manager is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
if force_refresh:
|
|
110
|
+
self._auth_manager.refresh_tokens()
|
|
111
|
+
|
|
112
|
+
return self._auth_manager.id_token
|
|
113
|
+
|
|
114
|
+
def _try_module_credentials(self, force_refresh: bool) -> str | None:
|
|
115
|
+
"""Try to get ID token from module-level CREDENTIALS."""
|
|
116
|
+
import tokentoss
|
|
117
|
+
|
|
118
|
+
creds = tokentoss.CREDENTIALS
|
|
119
|
+
if creds is None:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
if force_refresh or (hasattr(creds, "expired") and creds.expired):
|
|
123
|
+
creds.refresh(Request())
|
|
124
|
+
|
|
125
|
+
return getattr(creds, "id_token", None)
|
|
126
|
+
|
|
127
|
+
def _try_storage(self) -> str | None:
|
|
128
|
+
"""Try to get a non-expired ID token from file storage.
|
|
129
|
+
|
|
130
|
+
Note: Storage alone cannot refresh tokens (no client config available).
|
|
131
|
+
If the stored token is expired, returns None to let the caller raise.
|
|
132
|
+
"""
|
|
133
|
+
storage = self._get_fallback_storage()
|
|
134
|
+
try:
|
|
135
|
+
token_data = storage.load()
|
|
136
|
+
except Exception:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
if token_data is None:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
if token_data.is_expired:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
return token_data.id_token or None
|
|
146
|
+
|
|
147
|
+
def _build_url(self, path: str) -> str:
|
|
148
|
+
"""Construct full URL from base_url and path.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
path: Absolute URL or relative path.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Full URL string.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If path is relative and no base_url is set.
|
|
158
|
+
"""
|
|
159
|
+
if path.startswith(("http://", "https://")):
|
|
160
|
+
return path
|
|
161
|
+
|
|
162
|
+
if self.base_url is None:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"Relative path {path!r} requires a base_url. "
|
|
165
|
+
"Pass base_url to IAPClient() or use an absolute URL."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return f"{self.base_url}/{path.lstrip('/')}"
|
|
169
|
+
|
|
170
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> requests.Response:
|
|
171
|
+
"""Make an authenticated request with auto-retry on 401.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
method: HTTP method (GET, POST, etc.).
|
|
175
|
+
path: URL path or absolute URL.
|
|
176
|
+
**kwargs: Passed to requests.Session.request().
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
requests.Response object.
|
|
180
|
+
"""
|
|
181
|
+
url = self._build_url(path)
|
|
182
|
+
|
|
183
|
+
# Set timeout if not explicitly provided
|
|
184
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
185
|
+
|
|
186
|
+
# Get token and make request
|
|
187
|
+
id_token = self._get_id_token()
|
|
188
|
+
headers = kwargs.pop("headers", None) or {}
|
|
189
|
+
headers["Authorization"] = f"Bearer {id_token}"
|
|
190
|
+
kwargs["headers"] = headers
|
|
191
|
+
|
|
192
|
+
response = self._session.request(method, url, **kwargs)
|
|
193
|
+
|
|
194
|
+
# Retry once on 401 with forced refresh
|
|
195
|
+
if response.status_code == 401:
|
|
196
|
+
try:
|
|
197
|
+
refreshed_token = self._get_id_token(force_refresh=True)
|
|
198
|
+
headers["Authorization"] = f"Bearer {refreshed_token}"
|
|
199
|
+
kwargs["headers"] = headers
|
|
200
|
+
response = self._session.request(method, url, **kwargs)
|
|
201
|
+
except (NoCredentialsError, Exception):
|
|
202
|
+
pass # Return original 401 response
|
|
203
|
+
|
|
204
|
+
return response
|
|
205
|
+
|
|
206
|
+
# -- Public HTTP methods --
|
|
207
|
+
|
|
208
|
+
def get(self, path: str, **kwargs: Any) -> requests.Response:
|
|
209
|
+
"""GET request with IAP authentication."""
|
|
210
|
+
return self._request("GET", path, **kwargs)
|
|
211
|
+
|
|
212
|
+
def post(self, path: str, **kwargs: Any) -> requests.Response:
|
|
213
|
+
"""POST request with IAP authentication."""
|
|
214
|
+
return self._request("POST", path, **kwargs)
|
|
215
|
+
|
|
216
|
+
def put(self, path: str, **kwargs: Any) -> requests.Response:
|
|
217
|
+
"""PUT request with IAP authentication."""
|
|
218
|
+
return self._request("PUT", path, **kwargs)
|
|
219
|
+
|
|
220
|
+
def delete(self, path: str, **kwargs: Any) -> requests.Response:
|
|
221
|
+
"""DELETE request with IAP authentication."""
|
|
222
|
+
return self._request("DELETE", path, **kwargs)
|
|
223
|
+
|
|
224
|
+
def patch(self, path: str, **kwargs: Any) -> requests.Response:
|
|
225
|
+
"""PATCH request with IAP authentication."""
|
|
226
|
+
return self._request("PATCH", path, **kwargs)
|
|
227
|
+
|
|
228
|
+
def get_json(self, path: str, **kwargs: Any) -> Any:
|
|
229
|
+
"""GET request, return parsed JSON. Raises on non-2xx status."""
|
|
230
|
+
response = self.get(path, **kwargs)
|
|
231
|
+
response.raise_for_status()
|
|
232
|
+
return response.json()
|
|
233
|
+
|
|
234
|
+
def post_json(self, path: str, json: Any = None, **kwargs: Any) -> Any:
|
|
235
|
+
"""POST with JSON body, return parsed JSON. Raises on non-2xx status."""
|
|
236
|
+
response = self.post(path, json=json, **kwargs)
|
|
237
|
+
response.raise_for_status()
|
|
238
|
+
return response.json()
|
|
239
|
+
|
|
240
|
+
# -- Lifecycle --
|
|
241
|
+
|
|
242
|
+
def close(self) -> None:
|
|
243
|
+
"""Close the underlying requests session."""
|
|
244
|
+
self._session.close()
|
|
245
|
+
|
|
246
|
+
def __enter__(self) -> IAPClient:
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def __exit__(self, *args: Any) -> None:
|
|
250
|
+
self.close()
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Configuration widget for setting up OAuth client credentials in Jupyter notebooks.
|
|
2
|
+
|
|
3
|
+
Provides a password-safe input widget so credentials are entered at runtime
|
|
4
|
+
and never appear in .ipynb source or version control.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from tokentoss import ConfigureWidget
|
|
8
|
+
display(ConfigureWidget())
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import anywidget
|
|
14
|
+
import traitlets
|
|
15
|
+
|
|
16
|
+
from .setup import configure
|
|
17
|
+
|
|
18
|
+
_ESM = """
|
|
19
|
+
function render({ model, el }) {
|
|
20
|
+
const container = document.createElement('div');
|
|
21
|
+
container.className = 'tokentoss-configure';
|
|
22
|
+
|
|
23
|
+
// Client ID field
|
|
24
|
+
const idLabel = document.createElement('label');
|
|
25
|
+
idLabel.className = 'tokentoss-configure-label';
|
|
26
|
+
idLabel.textContent = 'Client ID';
|
|
27
|
+
const idInput = document.createElement('input');
|
|
28
|
+
idInput.type = 'text';
|
|
29
|
+
idInput.className = 'tokentoss-configure-input';
|
|
30
|
+
idInput.placeholder = '123456789.apps.googleusercontent.com';
|
|
31
|
+
|
|
32
|
+
// Client Secret field
|
|
33
|
+
const secretLabel = document.createElement('label');
|
|
34
|
+
secretLabel.className = 'tokentoss-configure-label';
|
|
35
|
+
secretLabel.textContent = 'Client Secret';
|
|
36
|
+
const secretInput = document.createElement('input');
|
|
37
|
+
secretInput.type = 'password';
|
|
38
|
+
secretInput.className = 'tokentoss-configure-input';
|
|
39
|
+
secretInput.placeholder = 'GOCSPX-...';
|
|
40
|
+
|
|
41
|
+
// Advanced (optional) section
|
|
42
|
+
const advancedHeader = document.createElement('div');
|
|
43
|
+
advancedHeader.className = 'tokentoss-configure-advanced-header';
|
|
44
|
+
advancedHeader.innerHTML = '▶ Advanced (optional)';
|
|
45
|
+
let advancedOpen = false;
|
|
46
|
+
|
|
47
|
+
const advancedContent = document.createElement('div');
|
|
48
|
+
advancedContent.className = 'tokentoss-configure-advanced-content';
|
|
49
|
+
advancedContent.style.display = 'none';
|
|
50
|
+
|
|
51
|
+
const projectLabel = document.createElement('label');
|
|
52
|
+
projectLabel.className = 'tokentoss-configure-label';
|
|
53
|
+
projectLabel.textContent = 'Project ID';
|
|
54
|
+
const projectInput = document.createElement('input');
|
|
55
|
+
projectInput.type = 'text';
|
|
56
|
+
projectInput.className = 'tokentoss-configure-input';
|
|
57
|
+
projectInput.placeholder = 'my-gcp-project';
|
|
58
|
+
|
|
59
|
+
advancedContent.appendChild(projectLabel);
|
|
60
|
+
advancedContent.appendChild(projectInput);
|
|
61
|
+
|
|
62
|
+
advancedHeader.addEventListener('click', () => {
|
|
63
|
+
advancedOpen = !advancedOpen;
|
|
64
|
+
advancedContent.style.display = advancedOpen ? 'block' : 'none';
|
|
65
|
+
advancedHeader.innerHTML = (advancedOpen ? '▼' : '▶') + ' Advanced (optional)';
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Submit button
|
|
69
|
+
const button = document.createElement('button');
|
|
70
|
+
button.className = 'tokentoss-configure-button';
|
|
71
|
+
button.textContent = 'Configure';
|
|
72
|
+
|
|
73
|
+
// Status display
|
|
74
|
+
const statusEl = document.createElement('div');
|
|
75
|
+
statusEl.className = 'tokentoss-configure-status';
|
|
76
|
+
|
|
77
|
+
// Assemble DOM
|
|
78
|
+
container.appendChild(idLabel);
|
|
79
|
+
container.appendChild(idInput);
|
|
80
|
+
container.appendChild(secretLabel);
|
|
81
|
+
container.appendChild(secretInput);
|
|
82
|
+
container.appendChild(advancedHeader);
|
|
83
|
+
container.appendChild(advancedContent);
|
|
84
|
+
container.appendChild(button);
|
|
85
|
+
container.appendChild(statusEl);
|
|
86
|
+
el.appendChild(container);
|
|
87
|
+
|
|
88
|
+
function updateStatus() {
|
|
89
|
+
const status = model.get('status');
|
|
90
|
+
const configured = model.get('configured');
|
|
91
|
+
statusEl.textContent = status;
|
|
92
|
+
statusEl.className = 'tokentoss-configure-status' +
|
|
93
|
+
(configured ? ' tokentoss-configure-success' : '');
|
|
94
|
+
if (status.startsWith('Error')) {
|
|
95
|
+
statusEl.className = 'tokentoss-configure-status tokentoss-configure-error';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
button.addEventListener('click', () => {
|
|
100
|
+
model.set('client_id', idInput.value);
|
|
101
|
+
model.set('client_secret', secretInput.value);
|
|
102
|
+
model.set('project_id', projectInput.value);
|
|
103
|
+
model.set('_submit', model.get('_submit') + 1);
|
|
104
|
+
model.save_changes();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
model.on('change:status', updateStatus);
|
|
108
|
+
model.on('change:configured', updateStatus);
|
|
109
|
+
|
|
110
|
+
updateStatus();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default { render };
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
_CSS = """
|
|
117
|
+
.tokentoss-configure {
|
|
118
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
119
|
+
padding: 16px;
|
|
120
|
+
border: 1px solid #e5e7eb;
|
|
121
|
+
border-radius: 8px;
|
|
122
|
+
background: #ffffff;
|
|
123
|
+
max-width: 400px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.tokentoss-configure-label {
|
|
127
|
+
display: block;
|
|
128
|
+
font-size: 13px;
|
|
129
|
+
font-weight: 500;
|
|
130
|
+
color: #374151;
|
|
131
|
+
margin-bottom: 4px;
|
|
132
|
+
margin-top: 12px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.tokentoss-configure-label:first-child {
|
|
136
|
+
margin-top: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.tokentoss-configure-input {
|
|
140
|
+
width: 100%;
|
|
141
|
+
padding: 8px 12px;
|
|
142
|
+
font-size: 13px;
|
|
143
|
+
border: 1px solid #d1d5db;
|
|
144
|
+
border-radius: 4px;
|
|
145
|
+
box-sizing: border-box;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.tokentoss-configure-input:focus {
|
|
149
|
+
outline: none;
|
|
150
|
+
border-color: #4285f4;
|
|
151
|
+
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.tokentoss-configure-button {
|
|
155
|
+
margin-top: 16px;
|
|
156
|
+
padding: 10px 20px;
|
|
157
|
+
font-size: 14px;
|
|
158
|
+
font-weight: 500;
|
|
159
|
+
color: #ffffff;
|
|
160
|
+
background: #4285f4;
|
|
161
|
+
border: none;
|
|
162
|
+
border-radius: 4px;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
transition: background 0.2s;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.tokentoss-configure-button:hover {
|
|
168
|
+
background: #3574e2;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.tokentoss-configure-status {
|
|
172
|
+
margin-top: 12px;
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
color: #6b7280;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.tokentoss-configure-success {
|
|
178
|
+
color: #059669;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.tokentoss-configure-error {
|
|
182
|
+
color: #dc2626;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.tokentoss-configure-advanced-header {
|
|
186
|
+
margin-top: 16px;
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
color: #6b7280;
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
user-select: none;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.tokentoss-configure-advanced-header:hover {
|
|
194
|
+
color: #374151;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.tokentoss-configure-advanced-content {
|
|
198
|
+
margin-top: 4px;
|
|
199
|
+
}
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ConfigureWidget(anywidget.AnyWidget):
|
|
204
|
+
"""Widget for configuring OAuth client credentials in Jupyter notebooks.
|
|
205
|
+
|
|
206
|
+
Provides password-style input fields for client_id and client_secret,
|
|
207
|
+
so credentials are entered at runtime and never appear in notebook source.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
from tokentoss import ConfigureWidget
|
|
211
|
+
display(ConfigureWidget())
|
|
212
|
+
# Enter credentials and click Configure
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
client_id = traitlets.Unicode("").tag(sync=True)
|
|
216
|
+
client_secret = traitlets.Unicode("").tag(sync=True)
|
|
217
|
+
project_id = traitlets.Unicode("").tag(sync=True)
|
|
218
|
+
status = traitlets.Unicode("Enter credentials").tag(sync=True)
|
|
219
|
+
configured = traitlets.Bool(False).tag(sync=True)
|
|
220
|
+
_submit = traitlets.Int(0).tag(sync=True)
|
|
221
|
+
|
|
222
|
+
_esm = _ESM
|
|
223
|
+
_css = _CSS
|
|
224
|
+
|
|
225
|
+
def __init__(self, **kwargs):
|
|
226
|
+
super().__init__(**kwargs)
|
|
227
|
+
self.observe(self._on_submit, names=["_submit"])
|
|
228
|
+
|
|
229
|
+
def _on_submit(self, change):
|
|
230
|
+
"""Handle submit button press."""
|
|
231
|
+
if change["new"] == 0:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
client_id = self.client_id.strip()
|
|
235
|
+
client_secret = self.client_secret.strip()
|
|
236
|
+
|
|
237
|
+
if not client_id or not client_secret:
|
|
238
|
+
self.status = "Error: both Client ID and Client Secret are required"
|
|
239
|
+
self.configured = False
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
project_id = self.project_id.strip() or None
|
|
244
|
+
path = configure(
|
|
245
|
+
client_id=client_id,
|
|
246
|
+
client_secret=client_secret,
|
|
247
|
+
project_id=project_id,
|
|
248
|
+
)
|
|
249
|
+
self.status = f"Configured! Saved to {path}"
|
|
250
|
+
self.configured = True
|
|
251
|
+
except Exception as e:
|
|
252
|
+
self.status = f"Error: {e}"
|
|
253
|
+
self.configured = False
|
tokentoss/exceptions.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Custom exceptions for tokentoss."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TokenTossError(Exception):
|
|
5
|
+
"""Base exception for tokentoss."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NoCredentialsError(TokenTossError):
|
|
11
|
+
"""Raised when no valid credentials are found.
|
|
12
|
+
|
|
13
|
+
This typically means:
|
|
14
|
+
- No AuthManager was passed to IAPClient
|
|
15
|
+
- tokentoss.CREDENTIALS module variable is not set
|
|
16
|
+
- No token file exists at the default location
|
|
17
|
+
- TOKENTOSS_TOKEN_FILE environment variable is not set or file doesn't exist
|
|
18
|
+
|
|
19
|
+
To authenticate, use the GoogleAuthWidget:
|
|
20
|
+
from tokentoss import GoogleAuthWidget
|
|
21
|
+
widget = GoogleAuthWidget(client_secrets_path="./client_secrets.json")
|
|
22
|
+
display(widget)
|
|
23
|
+
# Click "Sign in with Google" and complete the flow
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, message: str | None = None):
|
|
27
|
+
if message is None:
|
|
28
|
+
message = (
|
|
29
|
+
"No valid credentials found. "
|
|
30
|
+
"Use GoogleAuthWidget to authenticate or provide credentials explicitly."
|
|
31
|
+
)
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TokenRefreshError(TokenTossError):
|
|
36
|
+
"""Raised when token refresh fails."""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TokenExchangeError(TokenTossError):
|
|
42
|
+
"""Raised when authorization code exchange fails."""
|
|
43
|
+
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StorageError(TokenTossError):
|
|
48
|
+
"""Raised when token storage operations fail."""
|
|
49
|
+
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class InsecureFilePermissionsWarning(UserWarning):
|
|
54
|
+
"""Warning issued when token file has insecure permissions."""
|
|
55
|
+
|
|
56
|
+
pass
|