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 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
- with self._http_client() as client:
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
- json={
701
+ data={
691
702
  "grant_type": "refresh_token",
692
- "refresh_token": self.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 self.api_key:
787
- headers["Authorization"] = f"Bearer {self.api_key}"
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.client = Client(self.api_key, streamlit_compatible=True)
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
- self.client = Client(self.api_key, streamlit_compatible=True)
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(token_response.get("access_token"))
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.55.0
3
+ Version: 0.56.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
@@ -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=1JW7c_yPR4YARYBo6E_mx6iN13pU0zyl8rs6oTxdiUc,65126
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=sJSVWRAY-CT3FuEPKA41h4S0BsaVFChs0M_oVIj0VFQ,16520
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.55.0.dist-info/METADATA,sha256=sRtgay1gBJG-kQVB0InAV0tnAPbZKFZaPl0NS5sQvpk,852
15
- sweatstack-0.55.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- sweatstack-0.55.0.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
17
- sweatstack-0.55.0.dist-info/RECORD,,
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,,