sweatstack 0.55.0__tar.gz → 0.56.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.
Files changed (32) hide show
  1. {sweatstack-0.55.0 → sweatstack-0.56.0}/CHANGELOG.md +6 -0
  2. {sweatstack-0.55.0 → sweatstack-0.56.0}/PKG-INFO +1 -1
  3. {sweatstack-0.55.0 → sweatstack-0.56.0}/docs/everything.rst +91 -2
  4. {sweatstack-0.55.0 → sweatstack-0.56.0}/pyproject.toml +1 -1
  5. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/client.py +34 -10
  6. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/streamlit.py +24 -6
  7. {sweatstack-0.55.0 → sweatstack-0.56.0}/.claude/settings.local.json +0 -0
  8. {sweatstack-0.55.0 → sweatstack-0.56.0}/.gitignore +0 -0
  9. {sweatstack-0.55.0 → sweatstack-0.56.0}/.python-version +0 -0
  10. {sweatstack-0.55.0 → sweatstack-0.56.0}/DEVELOPMENT.md +0 -0
  11. {sweatstack-0.55.0 → sweatstack-0.56.0}/Makefile +0 -0
  12. {sweatstack-0.55.0 → sweatstack-0.56.0}/README.md +0 -0
  13. {sweatstack-0.55.0 → sweatstack-0.56.0}/docs/conf.py +0 -0
  14. {sweatstack-0.55.0 → sweatstack-0.56.0}/docs/index.rst +0 -0
  15. {sweatstack-0.55.0 → sweatstack-0.56.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  16. {sweatstack-0.55.0 → sweatstack-0.56.0}/playground/README.md +0 -0
  17. {sweatstack-0.55.0 → sweatstack-0.56.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  18. {sweatstack-0.55.0 → sweatstack-0.56.0}/playground/Untitled.ipynb +0 -0
  19. {sweatstack-0.55.0 → sweatstack-0.56.0}/playground/hello.py +0 -0
  20. {sweatstack-0.55.0 → sweatstack-0.56.0}/playground/pyproject.toml +0 -0
  21. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  22. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/__init__.py +0 -0
  23. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/cli.py +0 -0
  24. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/constants.py +0 -0
  25. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/ipython_init.py +0 -0
  26. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  27. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/openapi_schemas.py +0 -0
  28. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/py.typed +0 -0
  29. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/schemas.py +0 -0
  30. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/sweatshell.py +0 -0
  31. {sweatstack-0.55.0 → sweatstack-0.56.0}/src/sweatstack/utils.py +0 -0
  32. {sweatstack-0.55.0 → sweatstack-0.56.0}/uv.lock +0 -0
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [0.56.0] - 2025-11-21
10
+
11
+ ### Fixed
12
+ - Fixed an issue where refreshing the token would not succeed with the Streamlit integration.
13
+
14
+
9
15
  ## [0.55.0] - 2025-10-24
10
16
 
11
17
  ### Added
@@ -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
@@ -163,9 +163,98 @@ Metric
163
163
  :undoc-members:
164
164
 
165
165
  sweatstack.openapi_schemas
166
- ------------------
166
+ --------------------------
167
+
168
+ Core Data Models
169
+ ~~~~~~~~~~~~~~~~
170
+
171
+ .. autoclass:: sweatstack.openapi_schemas.ActivitySummary
172
+ :members:
173
+ :undoc-members:
174
+ :show-inheritance:
175
+
176
+ .. autoclass:: sweatstack.openapi_schemas.ActivityDetails
177
+ :members:
178
+ :undoc-members:
179
+ :show-inheritance:
180
+
181
+ .. autoclass:: sweatstack.openapi_schemas.TraceDetails
182
+ :members:
183
+ :undoc-members:
184
+ :show-inheritance:
185
+
186
+ .. autoclass:: sweatstack.openapi_schemas.Lap
187
+ :members:
188
+ :undoc-members:
189
+ :show-inheritance:
190
+
191
+ Activity Summaries
192
+ ~~~~~~~~~~~~~~~~~~
193
+
194
+ .. autoclass:: sweatstack.openapi_schemas.ActivitySummarySummary
195
+ :members:
196
+ :undoc-members:
197
+ :show-inheritance:
198
+
199
+ .. autoclass:: sweatstack.openapi_schemas.PowerSummary
200
+ :members:
201
+ :undoc-members:
202
+ :show-inheritance:
203
+
204
+ .. autoclass:: sweatstack.openapi_schemas.SpeedSummary
205
+ :members:
206
+ :undoc-members:
207
+ :show-inheritance:
208
+
209
+ .. autoclass:: sweatstack.openapi_schemas.DistanceSummary
210
+ :members:
211
+ :undoc-members:
212
+ :show-inheritance:
213
+
214
+ .. autoclass:: sweatstack.openapi_schemas.ElevationSummary
215
+ :members:
216
+ :undoc-members:
217
+ :show-inheritance:
218
+
219
+ .. autoclass:: sweatstack.openapi_schemas.HeartRateSummary
220
+ :members:
221
+ :undoc-members:
222
+ :show-inheritance:
223
+
224
+ .. autoclass:: sweatstack.openapi_schemas.TemperatureSummary
225
+ :members:
226
+ :undoc-members:
227
+ :show-inheritance:
228
+
229
+ .. autoclass:: sweatstack.openapi_schemas.CoreTemperatureSummary
230
+ :members:
231
+ :undoc-members:
232
+ :show-inheritance:
233
+
234
+ .. autoclass:: sweatstack.openapi_schemas.Smo2Summary
235
+ :members:
236
+ :undoc-members:
237
+ :show-inheritance:
238
+
239
+ User & Authentication
240
+ ~~~~~~~~~~~~~~~~~~~~~
241
+
242
+ .. autoclass:: sweatstack.openapi_schemas.UserSummary
243
+ :members:
244
+ :undoc-members:
245
+ :show-inheritance:
246
+
247
+ .. autoclass:: sweatstack.openapi_schemas.UserInfoResponse
248
+ :members:
249
+ :undoc-members:
250
+ :show-inheritance:
251
+
252
+ .. autoclass:: sweatstack.openapi_schemas.TokenResponse
253
+ :members:
254
+ :undoc-members:
255
+ :show-inheritance:
167
256
 
168
- .. automodule:: sweatstack.openapi_schemas
257
+ .. autoclass:: sweatstack.openapi_schemas.BackfillStatus
169
258
  :members:
170
259
  :undoc-members:
171
260
  :show-inheritance:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.55.0"
3
+ version = "0.56.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
 
@@ -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
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes