sweatstack 0.42.0__py3-none-any.whl → 0.44.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.
- sweatstack/client.py +92 -4
- sweatstack/ipython_init.py +1 -5
- sweatstack/jupyterlab_oauth2_startup.py +1 -4
- sweatstack/streamlit.py +11 -7
- {sweatstack-0.42.0.dist-info → sweatstack-0.44.0.dist-info}/METADATA +2 -1
- {sweatstack-0.42.0.dist-info → sweatstack-0.44.0.dist-info}/RECORD +8 -8
- {sweatstack-0.42.0.dist-info → sweatstack-0.44.0.dist-info}/WHEEL +0 -0
- {sweatstack-0.42.0.dist-info → sweatstack-0.44.0.dist-info}/entry_points.txt +0 -0
sweatstack/client.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import contextlib
|
|
3
|
+
import json
|
|
3
4
|
import random
|
|
4
5
|
import hashlib
|
|
5
6
|
import logging
|
|
@@ -14,11 +15,13 @@ from functools import wraps
|
|
|
14
15
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
15
16
|
from importlib.metadata import version
|
|
16
17
|
from io import BytesIO
|
|
18
|
+
from pathlib import Path
|
|
17
19
|
from typing import Any, Generator, get_type_hints, List, Literal
|
|
18
20
|
from urllib.parse import parse_qs, urlparse
|
|
19
21
|
|
|
20
22
|
import httpx
|
|
21
23
|
import pandas as pd
|
|
24
|
+
from platformdirs import user_data_dir
|
|
22
25
|
|
|
23
26
|
from .constants import DEFAULT_URL
|
|
24
27
|
from .schemas import (
|
|
@@ -45,6 +48,45 @@ AUTH_SUCCESSFUL_RESPONSE = """<!DOCTYPE html>
|
|
|
45
48
|
OAUTH2_CLIENT_ID = "5382f68b0d254378"
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
class TokenStorageMixin:
|
|
52
|
+
"""Mixin for handling persistent token storage using platformdirs."""
|
|
53
|
+
|
|
54
|
+
def _get_token_file_path(self) -> Path:
|
|
55
|
+
"""Get the path to the token storage file."""
|
|
56
|
+
data_dir = user_data_dir("SweatStack", "SweatStack")
|
|
57
|
+
return Path(data_dir) / "tokens.json"
|
|
58
|
+
|
|
59
|
+
def _save_tokens(self, access_token: str, refresh_token: str) -> None:
|
|
60
|
+
"""Save tokens to the user data directory."""
|
|
61
|
+
token_file = self._get_token_file_path()
|
|
62
|
+
token_file.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
token_data = {
|
|
65
|
+
"access_token": access_token,
|
|
66
|
+
"refresh_token": refresh_token
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
with open(token_file, "w") as f:
|
|
70
|
+
json.dump(token_data, f, indent=2)
|
|
71
|
+
|
|
72
|
+
# Set restrictive permissions (user read/write only)
|
|
73
|
+
token_file.chmod(0o600)
|
|
74
|
+
|
|
75
|
+
def _load_persistent_tokens(self) -> tuple[str | None, str | None]:
|
|
76
|
+
"""Load tokens from the user data directory."""
|
|
77
|
+
token_file = self._get_token_file_path()
|
|
78
|
+
|
|
79
|
+
if not token_file.exists():
|
|
80
|
+
return None, None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with open(token_file, "r") as f:
|
|
84
|
+
token_data = json.load(f)
|
|
85
|
+
return token_data.get("access_token"), token_data.get("refresh_token")
|
|
86
|
+
except (json.JSONDecodeError, FileNotFoundError, KeyError):
|
|
87
|
+
return None, None
|
|
88
|
+
|
|
89
|
+
|
|
48
90
|
try:
|
|
49
91
|
__version__ = version("sweatstack")
|
|
50
92
|
except ImportError:
|
|
@@ -52,7 +94,7 @@ except ImportError:
|
|
|
52
94
|
|
|
53
95
|
|
|
54
96
|
class OAuth2Mixin:
|
|
55
|
-
def login(self):
|
|
97
|
+
def login(self, persist_api_key: bool = True):
|
|
56
98
|
"""Initiates the OAuth2 login flow for SweatStack authentication.
|
|
57
99
|
|
|
58
100
|
This method starts a local HTTP server to receive the OAuth2 callback,
|
|
@@ -62,6 +104,10 @@ class OAuth2Mixin:
|
|
|
62
104
|
The method uses PKCE (Proof Key for Code Exchange) for enhanced security
|
|
63
105
|
during the OAuth2 authorization code flow.
|
|
64
106
|
|
|
107
|
+
Args:
|
|
108
|
+
persist_api_key: Whether to save the API key to persistent storage for future use.
|
|
109
|
+
Defaults to True.
|
|
110
|
+
|
|
65
111
|
Returns:
|
|
66
112
|
None
|
|
67
113
|
|
|
@@ -144,10 +190,45 @@ class OAuth2Mixin:
|
|
|
144
190
|
self.jwt = token_response.get("access_token")
|
|
145
191
|
self.api_key = self.jwt
|
|
146
192
|
self.refresh_token = token_response.get("refresh_token")
|
|
193
|
+
|
|
194
|
+
if persist_api_key:
|
|
195
|
+
self._save_tokens(self.api_key, self.refresh_token)
|
|
147
196
|
print(f"SweatStack Python login successful.")
|
|
148
197
|
else:
|
|
149
198
|
raise Exception("SweatStack Python login failed. Please try again.")
|
|
150
199
|
|
|
200
|
+
def authenticate(self, *, persist_api_key: bool = True, force_login: bool = False) -> None:
|
|
201
|
+
"""Ensures the client is authenticated, either using existing tokens or by initiating login.
|
|
202
|
+
|
|
203
|
+
This method checks for authentication in the following order:
|
|
204
|
+
1. Current instance tokens (if already set)
|
|
205
|
+
2. Environment variables (SWEATSTACK_API_KEY, SWEATSTACK_REFRESH_TOKEN)
|
|
206
|
+
3. Persistent storage tokens
|
|
207
|
+
4. If none found or force_login is True, initiates OAuth2 login flow
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
persist_api_key: Whether to save tokens to persistent storage after login.
|
|
211
|
+
Defaults to True.
|
|
212
|
+
force_login: Whether to force a new login even if tokens are available.
|
|
213
|
+
Defaults to False.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
None
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
Exception: If the authentication process fails.
|
|
220
|
+
"""
|
|
221
|
+
if force_login:
|
|
222
|
+
self.login(persist_api_key=persist_api_key)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# Check if we already have valid tokens
|
|
226
|
+
if self.api_key:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
# If no tokens available, initiate login
|
|
230
|
+
self.login(persist_api_key=persist_api_key)
|
|
231
|
+
|
|
151
232
|
|
|
152
233
|
class DelegationMixin:
|
|
153
234
|
def _validate_user(self, user: str | UserSummary):
|
|
@@ -352,7 +433,7 @@ class DelegationMixin:
|
|
|
352
433
|
)
|
|
353
434
|
|
|
354
435
|
|
|
355
|
-
class Client(OAuth2Mixin, DelegationMixin):
|
|
436
|
+
class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
|
|
356
437
|
def __init__(
|
|
357
438
|
self,
|
|
358
439
|
api_key: str | None = None,
|
|
@@ -399,8 +480,10 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
399
480
|
def api_key(self) -> str:
|
|
400
481
|
if self._api_key is not None:
|
|
401
482
|
value = self._api_key
|
|
483
|
+
elif value := os.getenv("SWEATSTACK_API_KEY"):
|
|
484
|
+
pass
|
|
402
485
|
else:
|
|
403
|
-
value =
|
|
486
|
+
value, _ = self._load_persistent_tokens()
|
|
404
487
|
|
|
405
488
|
if value is None:
|
|
406
489
|
# A non-authenticated client is a potentially valid use-case.
|
|
@@ -416,8 +499,12 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
416
499
|
def refresh_token(self) -> str:
|
|
417
500
|
if self._refresh_token is not None:
|
|
418
501
|
return self._refresh_token
|
|
502
|
+
elif value := os.getenv("SWEATSTACK_REFRESH_TOKEN"):
|
|
503
|
+
pass
|
|
419
504
|
else:
|
|
420
|
-
|
|
505
|
+
_, value = self._load_persistent_tokens()
|
|
506
|
+
|
|
507
|
+
return value
|
|
421
508
|
|
|
422
509
|
@refresh_token.setter
|
|
423
510
|
def refresh_token(self, value: str):
|
|
@@ -1156,6 +1243,7 @@ def _generate_singleton_methods(method_names: List[str]) -> None:
|
|
|
1156
1243
|
_generate_singleton_methods(
|
|
1157
1244
|
[
|
|
1158
1245
|
"login",
|
|
1246
|
+
"authenticate",
|
|
1159
1247
|
|
|
1160
1248
|
"get_user",
|
|
1161
1249
|
"get_users",
|
sweatstack/ipython_init.py
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import time
|
|
2
|
-
|
|
3
1
|
import sweatstack as ss
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
print("\n")
|
|
7
5
|
print(">>>>>>>>>> Sweat Stack Initialization <<<<<<<<<")
|
|
8
6
|
print("Initializing....")
|
|
9
|
-
print("You will be redirected to your browser for authentication.\n")
|
|
10
|
-
time.sleep(2)
|
|
11
7
|
|
|
12
|
-
ss.
|
|
8
|
+
ss.authenticate()
|
|
@@ -5,7 +5,6 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
import sweatstack as ss
|
|
7
7
|
from jupyterlab.labapp import LabApp
|
|
8
|
-
from sweatstack.client import _default_client
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
def start_jupyterlab_with_oauth():
|
|
@@ -24,9 +23,7 @@ def start_jupyterlab_with_oauth():
|
|
|
24
23
|
if not target_dir.exists():
|
|
25
24
|
shutil.copytree(examples_dir, target_dir)
|
|
26
25
|
|
|
27
|
-
ss.
|
|
28
|
-
os.environ["SWEATSTACK_API_KEY"] = _default_client.api_key
|
|
29
|
-
os.environ["SWEATSTACK_REFRESH_TOKEN"] = _default_client.refresh_token
|
|
26
|
+
ss.authenticate()
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
return LabApp.launch_instance(argv=remaining_args)
|
sweatstack/streamlit.py
CHANGED
|
@@ -60,8 +60,9 @@ class StreamlitAuth:
|
|
|
60
60
|
def _running_on_streamlit_cloud(self):
|
|
61
61
|
return os.environ.get("HOSTNAME") == "streamlit"
|
|
62
62
|
|
|
63
|
-
def _show_sweatstack_login(self):
|
|
64
|
-
authorization_url = self.
|
|
63
|
+
def _show_sweatstack_login(self, login_label: str | None = None):
|
|
64
|
+
authorization_url = self.get_authorization_url()
|
|
65
|
+
login_label = login_label or "Connect with SweatStack"
|
|
65
66
|
if not self._running_on_streamlit_cloud():
|
|
66
67
|
st.markdown(
|
|
67
68
|
f"""
|
|
@@ -87,14 +88,14 @@ class StreamlitAuth:
|
|
|
87
88
|
border: none;
|
|
88
89
|
transition: all 0.3s ease;
|
|
89
90
|
cursor: pointer;"
|
|
90
|
-
>
|
|
91
|
+
>{login_label}</a>
|
|
91
92
|
""",
|
|
92
93
|
unsafe_allow_html=True,
|
|
93
94
|
)
|
|
94
95
|
else:
|
|
95
|
-
st.link_button(
|
|
96
|
+
st.link_button(login_label, authorization_url)
|
|
96
97
|
|
|
97
|
-
def
|
|
98
|
+
def get_authorization_url(self):
|
|
98
99
|
params = {
|
|
99
100
|
"client_id": self.client_id,
|
|
100
101
|
"redirect_uri": self.redirect_uri,
|
|
@@ -145,7 +146,7 @@ class StreamlitAuth:
|
|
|
145
146
|
"""
|
|
146
147
|
return self.api_key is not None
|
|
147
148
|
|
|
148
|
-
def authenticate(self):
|
|
149
|
+
def authenticate(self, login_label: str | None = None):
|
|
149
150
|
"""Authenticates the user with SweatStack.
|
|
150
151
|
|
|
151
152
|
This method handles the authentication flow for SweatStack in a Streamlit app.
|
|
@@ -157,6 +158,9 @@ class StreamlitAuth:
|
|
|
157
158
|
to the Streamlit app with an authorization code, which is exchanged for an
|
|
158
159
|
access token.
|
|
159
160
|
|
|
161
|
+
Args:
|
|
162
|
+
login_label: The label to display on the login button. Defaults to "Login with SweatStack".
|
|
163
|
+
|
|
160
164
|
Returns:
|
|
161
165
|
None
|
|
162
166
|
"""
|
|
@@ -170,7 +174,7 @@ class StreamlitAuth:
|
|
|
170
174
|
st.query_params.clear()
|
|
171
175
|
st.rerun()
|
|
172
176
|
else:
|
|
173
|
-
self._show_sweatstack_login()
|
|
177
|
+
self._show_sweatstack_login(login_label)
|
|
174
178
|
|
|
175
179
|
def select_user(self):
|
|
176
180
|
"""Displays a user selection dropdown and switches the client to the selected user.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sweatstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.44.0
|
|
4
4
|
Summary: The official Python client for SweatStack
|
|
5
5
|
Author-email: Aart Goossens <aart@gssns.io>
|
|
6
6
|
Requires-Python: >=3.9
|
|
7
7
|
Requires-Dist: httpx>=0.28.1
|
|
8
8
|
Requires-Dist: pandas>=2.2.3
|
|
9
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
9
10
|
Requires-Dist: pyarrow>=18.0.0
|
|
10
11
|
Requires-Dist: pydantic>=2.10.5
|
|
11
12
|
Provides-Extra: jupyter
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
sweatstack/__init__.py,sha256=tiVfgKlswRPaDMEy0gA7u8rveqEYZTA_kyB9lJ3J6Sc,21
|
|
2
2
|
sweatstack/cli.py,sha256=N1NWOgEZR2yaJvIXxo9qvp_jFlypZYb0nujpbVNYQ6A,720
|
|
3
|
-
sweatstack/client.py,sha256=
|
|
3
|
+
sweatstack/client.py,sha256=aOTzYsTVHWsWDTlpw79-7yH-lTRMg_CBn3tDVW1BaVw,45914
|
|
4
4
|
sweatstack/constants.py,sha256=fGO6ksOv5HeISv9lHRoYm4besW1GTveXS8YD3K0ljg0,41
|
|
5
|
-
sweatstack/ipython_init.py,sha256=
|
|
6
|
-
sweatstack/jupyterlab_oauth2_startup.py,sha256=
|
|
5
|
+
sweatstack/ipython_init.py,sha256=OtBB9dQvyLXklD4kA2x1swaVtU9u73fG4V4-zz4YRAg,139
|
|
6
|
+
sweatstack/jupyterlab_oauth2_startup.py,sha256=YcjXvzeZ459vL_dCkFi1IxX_RNAu80ZX9rwa0OXJfTM,1023
|
|
7
7
|
sweatstack/openapi_schemas.py,sha256=uS5p_ksdF605FNTOhJ_VPPUCxJHOVJwyIOwVfXS-wEI,37019
|
|
8
8
|
sweatstack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
sweatstack/schemas.py,sha256=d6KRMJalaABO61CPm3afs-hM6zUcwbGrZ5mc6SVuc54,3370
|
|
10
|
-
sweatstack/streamlit.py,sha256=
|
|
10
|
+
sweatstack/streamlit.py,sha256=_PER03s0dYu5eF1MZdewPDqSvYHqMr0lZLu_EnGit3Y,13257
|
|
11
11
|
sweatstack/sweatshell.py,sha256=MYLNcWbOdceqKJ3S0Pe8dwHXEeYsGJNjQoYUXpMTftA,333
|
|
12
12
|
sweatstack/utils.py,sha256=AwHRdC1ziOZ5o9RBIB21Uxm-DoClVRAJSVvgsmSmvps,1801
|
|
13
13
|
sweatstack/Sweat Stack examples/Getting started.ipynb,sha256=k2hiSffWecoQ0VxjdpDcgFzBXDQiYEebhnAYlu8cgX8,6335204
|
|
14
|
-
sweatstack-0.
|
|
15
|
-
sweatstack-0.
|
|
16
|
-
sweatstack-0.
|
|
17
|
-
sweatstack-0.
|
|
14
|
+
sweatstack-0.44.0.dist-info/METADATA,sha256=TyoWIhA3YVSgPHL0X95S4SuM9Am-sgbX5rMyfifhQ54,814
|
|
15
|
+
sweatstack-0.44.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
sweatstack-0.44.0.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
|
|
17
|
+
sweatstack-0.44.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|