altasigma 3.12.0.dev0__tar.gz → 3.13.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.
- {altasigma-3.12.0.dev0/altasigma.egg-info → altasigma-3.13.0}/PKG-INFO +1 -1
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/credentials/credential_utils.py +117 -35
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/git_helper.py +19 -3
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/utils/notebook.py +3 -1
- {altasigma-3.12.0.dev0 → altasigma-3.13.0/altasigma.egg-info}/PKG-INFO +1 -1
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/setup.py +1 -1
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/LICENSE +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/MANIFEST.in +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/README_pypi.md +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/config/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/config/config.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/config/http_session.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/credentials/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/credentials/token_refresher.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/initialize.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/io/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/io/augurdata.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/io/data_management.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/io/files.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/job_supervisor_abstract.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/job_supervisor_dev_mock.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/job_supervisor_helpers.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/job_supervisor_http.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/reports.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/progress_reporter/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/progress_reporter/progress_reporter.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/spark_session/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/spark_session/spark_session_util.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/utils/__init__.py +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma.egg-info/SOURCES.txt +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma.egg-info/dependency_links.txt +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma.egg-info/entry_points.txt +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma.egg-info/requires.txt +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma.egg-info/top_level.txt +0 -0
- {altasigma-3.12.0.dev0 → altasigma-3.13.0}/setup.cfg +0 -0
|
@@ -55,6 +55,95 @@ def _decode_jwt_azp(token: str) -> str:
|
|
|
55
55
|
return ''
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
class ReauthRequired(Exception):
|
|
59
|
+
"""The dashboard responded that the user must (re)complete the git OAuth flow.
|
|
60
|
+
|
|
61
|
+
Headless callers (e.g. the notebook-git-api sidecar via the
|
|
62
|
+
``ALTASIGMA_ACCESS_TOKEN`` env var) cannot perform that flow themselves —
|
|
63
|
+
they should propagate this exception to the layer that owns the user's UI
|
|
64
|
+
so it can surface ``authorize_url``. The interactive notebook path catches
|
|
65
|
+
this exception and polls until the user completes the flow in the browser.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, authorize_url: str):
|
|
69
|
+
self.authorize_url = authorize_url
|
|
70
|
+
super().__init__(
|
|
71
|
+
f"git OAuth required; complete the flow at {authorize_url}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _fetch_git_credentials(host_url: str, access_token: str) -> tuple[str, str]:
|
|
76
|
+
"""
|
|
77
|
+
Mint a fresh per-user git access token via the dashboard's git-oauth2 endpoint
|
|
78
|
+
and shape it into the (username, password) pair git's credential helper
|
|
79
|
+
protocol expects.
|
|
80
|
+
|
|
81
|
+
Pure HTTP — never blocks, never polls, never prints. The interactive
|
|
82
|
+
polling-on-reauth loop lives in ``_await_git_oauth_completion`` (used by the
|
|
83
|
+
notebook flow in ``CredentialUtils.get_git_credentials``); headless callers
|
|
84
|
+
receive ``ReauthRequired`` and decide for themselves how to surface it.
|
|
85
|
+
|
|
86
|
+
Consumers: ``CredentialUtils.get_git_credentials`` (this package, notebook
|
|
87
|
+
path) and the ``notebook-git-api`` sidecar (imports this symbol directly).
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
host_url (str): The AltaSigma installation host URL (no trailing slash needed).
|
|
91
|
+
access_token (str): A Keycloak bearer authorised for the dashboard.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
tuple: (username, password)
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ReauthRequired: User has no usable grant; carries the authorize URL.
|
|
98
|
+
Exception: Any other non-200 response.
|
|
99
|
+
"""
|
|
100
|
+
headers = {'Authorization': f'Bearer {access_token}'}
|
|
101
|
+
url = f'{host_url}/api/git/oauth/access-token'
|
|
102
|
+
response = _http_session().get(url, headers=headers)
|
|
103
|
+
|
|
104
|
+
if _is_reauth_required(response):
|
|
105
|
+
# User has no usable grant (never connected, revoked, or expired refresh).
|
|
106
|
+
raise ReauthRequired(response.json()["details"]["authorizeUrl"])
|
|
107
|
+
|
|
108
|
+
if response.status_code == 200:
|
|
109
|
+
# Gitea-only for now: token goes in the password field, username can be
|
|
110
|
+
# anything non-empty. When other providers (GitLab, Bitbucket, ...) are
|
|
111
|
+
# added, the dashboard should return both fields and the helper should
|
|
112
|
+
# just forward them.
|
|
113
|
+
return "oauth2", response.json()["accessToken"]
|
|
114
|
+
|
|
115
|
+
raise Exception(
|
|
116
|
+
f"ERROR: Unexpected response from dashboard: {response.status_code}, {response.text}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_reauth_required(response) -> bool:
|
|
120
|
+
"""True if the response is the dashboard's 409 reauth-required signal."""
|
|
121
|
+
if response.status_code != 409:
|
|
122
|
+
return False
|
|
123
|
+
try:
|
|
124
|
+
return response.json().get("details", {}).get("reason") == "reauth_required"
|
|
125
|
+
except ValueError:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _await_git_oauth_completion(host_url: str, access_token: str, authorize_url: str) -> tuple[str, str]:
|
|
130
|
+
"""Interactive notebook helper: show the OAuth authorize URL once, then
|
|
131
|
+
poll the dashboard until the user completes the flow in the browser."""
|
|
132
|
+
# Plain URL — OSC 8 escapes show as garbage in Jupyter `!` cell output.
|
|
133
|
+
display_or_print(
|
|
134
|
+
msg_notebook=HTML(
|
|
135
|
+
f'Please authorize git access: '
|
|
136
|
+
f'<a href="{authorize_url}" target="_blank">{authorize_url}</a>'),
|
|
137
|
+
msg_other=f"Please authorize git access:\n{authorize_url}")
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
sleep(2)
|
|
141
|
+
try:
|
|
142
|
+
return _fetch_git_credentials(host_url, access_token)
|
|
143
|
+
except ReauthRequired:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
|
|
58
147
|
class CredentialUtils:
|
|
59
148
|
"""
|
|
60
149
|
Manages authentication and data source credential access.
|
|
@@ -146,43 +235,32 @@ class CredentialUtils:
|
|
|
146
235
|
raise Exception(
|
|
147
236
|
f"ERROR: Unexpected response from dataman: {data_sources_request.status_code}, {data_sources_request.text}")
|
|
148
237
|
|
|
149
|
-
def get_git_credentials(self
|
|
238
|
+
def get_git_credentials(self) -> tuple[str, str]:
|
|
150
239
|
"""
|
|
151
|
-
Retrieve
|
|
240
|
+
Retrieve git credentials for the user's connected provider from the AltaSigma dashboard.
|
|
152
241
|
|
|
153
242
|
Acquires a Keycloak bearer via the same token flow as `get_credentials`,
|
|
154
|
-
then asks the dashboard
|
|
155
|
-
the
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
host (str): The git host the user wants credentials for (e.g. "gitea.altasigma.internal").
|
|
243
|
+
then asks the dashboard to mint a fresh per-user git access token for the
|
|
244
|
+
single dashboard-configured provider. If the user hasn't completed the
|
|
245
|
+
git OAuth flow yet (or their grant has expired), prints the authorize URL
|
|
246
|
+
and polls the dashboard until they complete the flow in the browser.
|
|
159
247
|
|
|
160
248
|
Returns:
|
|
161
|
-
tuple: (username, password
|
|
249
|
+
tuple: (username, password)
|
|
162
250
|
|
|
163
251
|
Raises:
|
|
164
|
-
Exception: If the
|
|
165
|
-
or if the dashboard returns any other non-2xx status.
|
|
252
|
+
Exception: If the dashboard returns any non-200 / non-reauth status.
|
|
166
253
|
"""
|
|
167
254
|
(access_token, _refresh_token) = self._existing_token()
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
msg_other="Successfully obtained git credentials")
|
|
178
|
-
return res["username"], res["password"], res["expiresAt"]
|
|
179
|
-
elif response.status_code == 404:
|
|
180
|
-
raise Exception(
|
|
181
|
-
f"Git host '{host}' is not connected in the AltaSigma dashboard. "
|
|
182
|
-
f"Connect Gitea under Account → Integrations and retry.")
|
|
183
|
-
else:
|
|
184
|
-
raise Exception(
|
|
185
|
-
f"ERROR: Unexpected response from dashboard: {response.status_code}, {response.text}")
|
|
255
|
+
try:
|
|
256
|
+
username, password = _fetch_git_credentials(self.dashboard_api_host, access_token)
|
|
257
|
+
except ReauthRequired as e:
|
|
258
|
+
username, password = _await_git_oauth_completion(
|
|
259
|
+
self.dashboard_api_host, access_token, e.authorize_url)
|
|
260
|
+
display_or_print(
|
|
261
|
+
msg_notebook=HTML("Successfully obtained git credentials"),
|
|
262
|
+
msg_other="Successfully obtained git credentials")
|
|
263
|
+
return username, password
|
|
186
264
|
|
|
187
265
|
def _refresh_token(self, refresh_token: str) -> tuple[str, str]:
|
|
188
266
|
"""
|
|
@@ -370,10 +448,11 @@ class CredentialUtils:
|
|
|
370
448
|
device_response = response.json()
|
|
371
449
|
device_code = device_response["device_code"]
|
|
372
450
|
authorization_url = device_response["verification_uri_complete"]
|
|
451
|
+
# URL on its own line so terminals don't soft-wrap it mid-token.
|
|
373
452
|
display_or_print(
|
|
374
453
|
msg_notebook=HTML(
|
|
375
454
|
f'Please authenticate using the following link: <a href="{authorization_url}" target="_blank">{authorization_url}</a>'),
|
|
376
|
-
msg_other=f'Please authenticate using the following link
|
|
455
|
+
msg_other=f'Please authenticate using the following link:\n{authorization_url}')
|
|
377
456
|
else:
|
|
378
457
|
raise Exception(f"ERROR: Unexpected response {response.status_code}, {response.text}")
|
|
379
458
|
|
|
@@ -401,13 +480,16 @@ class CredentialUtils:
|
|
|
401
480
|
break
|
|
402
481
|
elif response.status_code == 400 and response.json().get('error') == 'authorization_pending':
|
|
403
482
|
if is_in_notebook():
|
|
483
|
+
# Re-render URL + animated dots so the cell stays compact
|
|
484
|
+
# (single output, not appended on every poll).
|
|
404
485
|
clear_output(wait=True)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
486
|
+
display_or_print(msg_notebook=HTML(
|
|
487
|
+
f'Please authenticate using the following link: <a href="{authorization_url}" target="_blank">{authorization_url}</a>'),
|
|
488
|
+
msg_other=f'Please authenticate using the following link: {authorization_url}')
|
|
489
|
+
display_or_print(
|
|
490
|
+
msg_notebook=HTML(f"Authorization pending {'.' * i}"),
|
|
491
|
+
msg_other=f"Authorization pending {'.' * i}")
|
|
492
|
+
# Non-notebook: URL was printed once before the polling loop; stay quiet here.
|
|
411
493
|
i = (i + 1) % 4
|
|
412
494
|
else:
|
|
413
495
|
raise Exception(
|
|
@@ -9,6 +9,14 @@ Installed as the console script `git-credential-altasigma`. Once a user runs
|
|
|
9
9
|
`git config credential.helper altasigma`, git will resolve that to this script
|
|
10
10
|
on PATH and invoke it with `get`, `store`, or `erase` subcommands.
|
|
11
11
|
|
|
12
|
+
The helper resolves the dashboard bearer through ``CredentialUtils``
|
|
13
|
+
(refresh-token cache + Keycloak refresh / device flow), so it is intended for
|
|
14
|
+
interactive (notebook) callers where the device-auth URL can be shown to a
|
|
15
|
+
human. Headless callers (e.g. the notebook-git-api sidecar) should call
|
|
16
|
+
``altasigma.credentials.credential_utils._fetch_git_credentials`` directly from
|
|
17
|
+
Python with the inbound bearer so ``ReauthRequired`` can be surfaced as a
|
|
18
|
+
structured error.
|
|
19
|
+
|
|
12
20
|
Stdout is reserved exclusively for the two `username=...` / `password=...` lines
|
|
13
21
|
that git expects on success. Everything else (logs, errors) goes to stderr, and
|
|
14
22
|
any failure exits non-zero with empty stdout so git treats it as "no credentials"
|
|
@@ -67,14 +75,22 @@ def main() -> int:
|
|
|
67
75
|
logger.error("git credential request missing required 'host' field")
|
|
68
76
|
return 1
|
|
69
77
|
|
|
78
|
+
# Redirect stdout -> stderr while fetching credentials so that any user-facing
|
|
79
|
+
# prints from CredentialUtils (e.g. the device-flow URL, "Authorization pending")
|
|
80
|
+
# don't pollute git's credential-helper input. Git parses each line on the
|
|
81
|
+
# helper's stdout as `key=value`; anything else makes git warn and close the pipe.
|
|
82
|
+
real_stdout = sys.stdout
|
|
83
|
+
sys.stdout = sys.stderr
|
|
70
84
|
try:
|
|
71
|
-
username, password
|
|
85
|
+
username, password = CredentialUtils().get_git_credentials()
|
|
72
86
|
except Exception as exc:
|
|
73
87
|
logger.error("failed to obtain git credentials for host %s: %s", host, exc)
|
|
74
88
|
return 1
|
|
89
|
+
finally:
|
|
90
|
+
sys.stdout = real_stdout
|
|
75
91
|
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
real_stdout.write(f"username={username}\n")
|
|
93
|
+
real_stdout.write(f"password={password}\n")
|
|
78
94
|
return 0
|
|
79
95
|
|
|
80
96
|
|
|
@@ -61,4 +61,6 @@ def display_or_print(msg_notebook, msg_other):
|
|
|
61
61
|
if is_in_notebook():
|
|
62
62
|
display(msg_notebook)
|
|
63
63
|
else:
|
|
64
|
-
|
|
64
|
+
# flush=True so output reaches the parent (e.g. git invoking the credential helper)
|
|
65
|
+
# immediately, instead of sitting in Python's pipe buffer until process exit.
|
|
66
|
+
print(msg_other, flush=True)
|
|
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
|
{altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/job_supervisor_abstract.py
RENAMED
|
File without changes
|
{altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/job_supervisor_dev_mock.py
RENAMED
|
File without changes
|
{altasigma-3.12.0.dev0 → altasigma-3.13.0}/altasigma/jobsupervisor/job_supervisor_helpers.py
RENAMED
|
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
|