sweatstack 0.55.0__py3-none-any.whl → 0.56.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 +34 -10
- sweatstack/streamlit.py +24 -6
- {sweatstack-0.55.0.dist-info → sweatstack-0.56.0.dist-info}/METADATA +1 -1
- {sweatstack-0.55.0.dist-info → sweatstack-0.56.0.dist-info}/RECORD +6 -6
- {sweatstack-0.55.0.dist-info → sweatstack-0.56.0.dist-info}/WHEEL +0 -0
- {sweatstack-0.55.0.dist-info → sweatstack-0.56.0.dist-info}/entry_points.txt +0 -0
sweatstack/client.py
CHANGED
|
@@ -669,6 +669,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
669
669
|
refresh_token: str | None = None,
|
|
670
670
|
url: str | None = None,
|
|
671
671
|
streamlit_compatible: bool = False,
|
|
672
|
+
client_id: str | None = None,
|
|
673
|
+
client_secret: str | None = None,
|
|
672
674
|
):
|
|
673
675
|
"""Initialize a SweatStack client.
|
|
674
676
|
|
|
@@ -682,18 +684,28 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
682
684
|
self.refresh_token = refresh_token
|
|
683
685
|
self.url = url
|
|
684
686
|
self.streamlit_compatible = streamlit_compatible
|
|
687
|
+
self.client_id = client_id or OAUTH2_CLIENT_ID
|
|
688
|
+
self.client_secret = client_secret
|
|
685
689
|
|
|
686
690
|
def _do_token_refresh(self, tz: str) -> str:
|
|
687
|
-
|
|
691
|
+
refresh_token = self.refresh_token
|
|
692
|
+
if refresh_token is None:
|
|
693
|
+
raise ValueError(
|
|
694
|
+
"Cannot refresh token: no refresh_token available. "
|
|
695
|
+
"If using Streamlit, ensure you're using StreamlitAuth which handles token refresh automatically."
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
with self._http_client(skip_token_check=True) as client:
|
|
688
699
|
response = client.post(
|
|
689
700
|
"/api/v1/oauth/token",
|
|
690
|
-
|
|
701
|
+
data={
|
|
691
702
|
"grant_type": "refresh_token",
|
|
692
|
-
"refresh_token":
|
|
703
|
+
"refresh_token": refresh_token,
|
|
693
704
|
"tz": tz,
|
|
705
|
+
"client_id": self.client_id,
|
|
706
|
+
"client_secret": self.client_secret,
|
|
694
707
|
},
|
|
695
708
|
)
|
|
696
|
-
|
|
697
709
|
self._raise_for_status(response)
|
|
698
710
|
return response.json()["access_token"]
|
|
699
711
|
|
|
@@ -701,12 +713,13 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
701
713
|
try:
|
|
702
714
|
body = decode_jwt_body(token)
|
|
703
715
|
# Margin in seconds to account for time to token validation of the next request
|
|
704
|
-
TOKEN_EXPIRY_MARGIN = 5
|
|
716
|
+
TOKEN_EXPIRY_MARGIN = 5 # 5 seconds. Meaning that if the token is within 5 seconds of expiring, it will be refreshed.
|
|
705
717
|
if body["exp"] - TOKEN_EXPIRY_MARGIN < time.time():
|
|
706
718
|
# Token is (almost) expired, refresh it
|
|
707
719
|
token = self._do_token_refresh(body["tz"])
|
|
708
720
|
self._api_key = token
|
|
709
|
-
except Exception:
|
|
721
|
+
except Exception as exception:
|
|
722
|
+
logging.warning("Exception checking token expiry: %s", exception)
|
|
710
723
|
# If token can't be decoded, just return as-is
|
|
711
724
|
# @TODO: This probably should be handled differently
|
|
712
725
|
pass
|
|
@@ -776,16 +789,27 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
776
789
|
self._url = value
|
|
777
790
|
|
|
778
791
|
@contextlib.contextmanager
|
|
779
|
-
def _http_client(self):
|
|
792
|
+
def _http_client(self, skip_token_check: bool = False):
|
|
780
793
|
"""
|
|
781
794
|
Creates an httpx client with the base URL and authentication headers pre-configured.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
skip_token_check: If True, uses the raw _api_key without triggering token expiry check.
|
|
798
|
+
This prevents recursive token refresh attempts.
|
|
782
799
|
"""
|
|
783
800
|
headers = {
|
|
784
801
|
"User-Agent": f"python-sweatstack/{__version__}",
|
|
785
802
|
}
|
|
786
|
-
if
|
|
787
|
-
|
|
788
|
-
|
|
803
|
+
if skip_token_check:
|
|
804
|
+
# Use raw token without triggering expiry check (used during refresh)
|
|
805
|
+
token = self._api_key
|
|
806
|
+
else:
|
|
807
|
+
# Normal path: may trigger token refresh
|
|
808
|
+
token = self.api_key
|
|
809
|
+
|
|
810
|
+
if token:
|
|
811
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
812
|
+
|
|
789
813
|
with httpx.Client(base_url=self.url, headers=headers, timeout=60) as client:
|
|
790
814
|
yield client
|
|
791
815
|
|
sweatstack/streamlit.py
CHANGED
|
@@ -115,7 +115,14 @@ class StreamlitAuth:
|
|
|
115
115
|
self.redirect_uri = redirect_uri or os.environ.get("SWEATSTACK_REDIRECT_URI")
|
|
116
116
|
|
|
117
117
|
self.api_key = st.session_state.get("sweatstack_api_key")
|
|
118
|
-
self.
|
|
118
|
+
self.refresh_token = st.session_state.get("sweatstack_refresh_token")
|
|
119
|
+
self.client = Client(
|
|
120
|
+
self.api_key,
|
|
121
|
+
refresh_token=self.refresh_token,
|
|
122
|
+
streamlit_compatible=True,
|
|
123
|
+
client_id=self.client_id,
|
|
124
|
+
client_secret=self.client_secret,
|
|
125
|
+
)
|
|
119
126
|
|
|
120
127
|
def logout_button(self):
|
|
121
128
|
"""Displays a logout button and handles user logout.
|
|
@@ -125,8 +132,10 @@ class StreamlitAuth:
|
|
|
125
132
|
"""
|
|
126
133
|
if st.button("Logout"):
|
|
127
134
|
self.api_key = None
|
|
135
|
+
self.refresh_token = None
|
|
128
136
|
self.client = Client(streamlit_compatible=True)
|
|
129
|
-
st.session_state.pop("sweatstack_api_key")
|
|
137
|
+
st.session_state.pop("sweatstack_api_key", None)
|
|
138
|
+
st.session_state.pop("sweatstack_refresh_token", None)
|
|
130
139
|
st.rerun()
|
|
131
140
|
|
|
132
141
|
def _running_on_streamlit_cloud(self):
|
|
@@ -193,15 +202,21 @@ class StreamlitAuth:
|
|
|
193
202
|
|
|
194
203
|
return authorization_url
|
|
195
204
|
|
|
196
|
-
def _set_api_key(self, api_key):
|
|
197
|
-
"""Sets the API key in instance and session state, then refreshes the client.
|
|
205
|
+
def _set_api_key(self, api_key, refresh_token=None):
|
|
206
|
+
"""Sets the API key and refresh token in instance and session state, then refreshes the client.
|
|
198
207
|
|
|
199
208
|
Args:
|
|
200
209
|
api_key: The API access token to set.
|
|
210
|
+
refresh_token: The refresh token to set. If None, keeps the existing refresh token.
|
|
201
211
|
"""
|
|
202
212
|
self.api_key = api_key
|
|
203
213
|
st.session_state["sweatstack_api_key"] = api_key
|
|
204
|
-
|
|
214
|
+
|
|
215
|
+
if refresh_token is not None:
|
|
216
|
+
self.refresh_token = refresh_token
|
|
217
|
+
st.session_state["sweatstack_refresh_token"] = refresh_token
|
|
218
|
+
|
|
219
|
+
self.client = Client(self.api_key, refresh_token=self.refresh_token, streamlit_compatible=True)
|
|
205
220
|
|
|
206
221
|
def _exchange_token(self, code):
|
|
207
222
|
"""Exchanges an authorization code for an access token.
|
|
@@ -230,7 +245,10 @@ class StreamlitAuth:
|
|
|
230
245
|
raise Exception(f"SweatStack Python login failed. Please try again.") from e
|
|
231
246
|
token_response = response.json()
|
|
232
247
|
|
|
233
|
-
self._set_api_key(
|
|
248
|
+
self._set_api_key(
|
|
249
|
+
token_response.get("access_token"),
|
|
250
|
+
refresh_token=token_response.get("refresh_token")
|
|
251
|
+
)
|
|
234
252
|
|
|
235
253
|
return
|
|
236
254
|
|
|
@@ -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=dTx7Cqpd56NH32-Ndc6vo1WnzsN1C9BfpFrchV9NTdQ,66366
|
|
4
4
|
sweatstack/constants.py,sha256=fGO6ksOv5HeISv9lHRoYm4besW1GTveXS8YD3K0ljg0,41
|
|
5
5
|
sweatstack/ipython_init.py,sha256=OtBB9dQvyLXklD4kA2x1swaVtU9u73fG4V4-zz4YRAg,139
|
|
6
6
|
sweatstack/jupyterlab_oauth2_startup.py,sha256=YcjXvzeZ459vL_dCkFi1IxX_RNAu80ZX9rwa0OXJfTM,1023
|
|
7
7
|
sweatstack/openapi_schemas.py,sha256=VvquBdbssdB9D1KeJYQCx51hy1Df4SS0PjzGWXcUaew,46221
|
|
8
8
|
sweatstack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
sweatstack/schemas.py,sha256=Xh9E8DjFx5NIEBnVqS6ixFVb0E06ANZbdOlnMofCpZw,4481
|
|
10
|
-
sweatstack/streamlit.py,sha256=
|
|
10
|
+
sweatstack/streamlit.py,sha256=fVTgTyb8a2c7IC-lCs32ocILvyj1dFXP4iQaTV9D4Is,17287
|
|
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.56.0.dist-info/METADATA,sha256=N732fwIaJz4h5uVfOCZUwSaMbaLk7CB1leB6dHpvpw4,852
|
|
15
|
+
sweatstack-0.56.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
16
|
+
sweatstack-0.56.0.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
|
|
17
|
+
sweatstack-0.56.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|