sweatstack 0.54.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.
- {sweatstack-0.54.0 → sweatstack-0.56.0}/.claude/settings.local.json +3 -2
- {sweatstack-0.54.0 → sweatstack-0.56.0}/.gitignore +3 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/CHANGELOG.md +13 -0
- sweatstack-0.56.0/Makefile +12 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/PKG-INFO +1 -1
- sweatstack-0.56.0/docs/conf.py +26 -0
- sweatstack-0.56.0/docs/everything.rst +260 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/pyproject.toml +5 -1
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/client.py +168 -16
- sweatstack-0.56.0/src/sweatstack/py.typed +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/schemas.py +36 -13
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/streamlit.py +127 -10
- {sweatstack-0.54.0 → sweatstack-0.56.0}/uv.lock +196 -7
- sweatstack-0.54.0/Makefile +0 -6
- {sweatstack-0.54.0 → sweatstack-0.56.0}/.python-version +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/README.md +0 -0
- /sweatstack-0.54.0/playground/README.md → /sweatstack-0.56.0/docs/index.rst +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- /sweatstack-0.54.0/src/sweatstack/py.typed → /sweatstack-0.56.0/playground/README.md +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/playground/Untitled.ipynb +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/playground/hello.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/playground/pyproject.toml +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.54.0 → sweatstack-0.56.0}/src/sweatstack/utils.py +0 -0
|
@@ -6,6 +6,19 @@ 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
|
+
|
|
15
|
+
## [0.55.0] - 2025-10-24
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Added a new `get_activity_awd()` method to the `ss.Client` class that allows for getting the accumulated work duration (AWD) data for a specific activity.
|
|
19
|
+
- Added a new `get_longitudinal_awd()` method to the `ss.Client` class that allows for getting the AWD data for a specific date range.
|
|
20
|
+
|
|
21
|
+
|
|
9
22
|
## [0.54.0] - 2025-09-11
|
|
10
23
|
|
|
11
24
|
### Added
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# So autodoc can import your package
|
|
5
|
+
sys.path.insert(0, os.path.abspath("..")) # adjust if needed
|
|
6
|
+
|
|
7
|
+
extensions = [
|
|
8
|
+
"sphinx.ext.autodoc",
|
|
9
|
+
"sphinx.ext.autosummary",
|
|
10
|
+
"sphinx.ext.napoleon", # if you use Google/NumPy docstrings
|
|
11
|
+
"sphinx_markdown_builder", # <-- this is the important one
|
|
12
|
+
"myst_parser", # optional, for .md sources
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
autosummary_generate = True
|
|
16
|
+
|
|
17
|
+
# Autodoc configuration
|
|
18
|
+
autodoc_default_options = {
|
|
19
|
+
'private-members': False, # Don't document private members (starting with _)
|
|
20
|
+
'special-members': False, # Don't document special members (like __init__ unless specified)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Optional nice-to-haves for markdown builder:
|
|
24
|
+
markdown_anchor_sections = True # add anchors to each section/function/class
|
|
25
|
+
markdown_anchor_signatures = True # add anchors to signatures
|
|
26
|
+
markdown_bullet = "*"
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Complete API Reference
|
|
2
|
+
======================
|
|
3
|
+
|
|
4
|
+
Environment Variables
|
|
5
|
+
=====================
|
|
6
|
+
|
|
7
|
+
The SweatStack client library uses environment variables for configuration.
|
|
8
|
+
These variables provide defaults for authentication, caching, and Streamlit integration.
|
|
9
|
+
|
|
10
|
+
Authentication
|
|
11
|
+
--------------
|
|
12
|
+
|
|
13
|
+
.. envvar:: SWEATSTACK_API_KEY
|
|
14
|
+
|
|
15
|
+
API access token for authenticating with SweatStack.
|
|
16
|
+
|
|
17
|
+
Automatically loaded by the Client if not provided during initialization.
|
|
18
|
+
Falls back to persistent storage if not found in environment.
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
export SWEATSTACK_API_KEY="your_token_here"
|
|
23
|
+
|
|
24
|
+
.. envvar:: SWEATSTACK_REFRESH_TOKEN
|
|
25
|
+
|
|
26
|
+
Refresh token used for automatic token renewal.
|
|
27
|
+
|
|
28
|
+
Loaded from environment, instance, or persistent storage. Used by the Client
|
|
29
|
+
to automatically refresh expired access tokens.
|
|
30
|
+
|
|
31
|
+
.. envvar:: SWEATSTACK_URL
|
|
32
|
+
|
|
33
|
+
Custom SweatStack instance URL.
|
|
34
|
+
|
|
35
|
+
Override the default SweatStack API URL. Useful for testing or
|
|
36
|
+
connecting to self-hosted instances.
|
|
37
|
+
|
|
38
|
+
Default: ``https://app.sweatstack.no``
|
|
39
|
+
|
|
40
|
+
Caching
|
|
41
|
+
-------
|
|
42
|
+
|
|
43
|
+
.. envvar:: SWEATSTACK_LOCAL_CACHE
|
|
44
|
+
|
|
45
|
+
Enable local filesystem caching of API responses.
|
|
46
|
+
|
|
47
|
+
Set to any truthy value (``1``, ``true``, ``yes``) to enable caching
|
|
48
|
+
of longitudinal data requests. Cached data persists across sessions.
|
|
49
|
+
Use ``client.clear_cache()`` to remove cached data.
|
|
50
|
+
|
|
51
|
+
Default: Disabled
|
|
52
|
+
|
|
53
|
+
Example::
|
|
54
|
+
|
|
55
|
+
export SWEATSTACK_LOCAL_CACHE=1
|
|
56
|
+
|
|
57
|
+
.. envvar:: SWEATSTACK_CACHE_DIR
|
|
58
|
+
|
|
59
|
+
Custom directory location for cached data.
|
|
60
|
+
|
|
61
|
+
Specify where to store cached API responses. If not set, defaults to
|
|
62
|
+
the system temp directory with a ``sweatstack/{user_id}`` subdirectory.
|
|
63
|
+
|
|
64
|
+
Default: System temp directory (e.g., ``/tmp/sweatstack/{user_id}``)
|
|
65
|
+
|
|
66
|
+
Example::
|
|
67
|
+
|
|
68
|
+
export SWEATSTACK_CACHE_DIR=/path/to/cache
|
|
69
|
+
|
|
70
|
+
Streamlit OAuth2
|
|
71
|
+
----------------
|
|
72
|
+
|
|
73
|
+
These environment variables are used by :class:`sweatstack.streamlit.StreamlitAuth`
|
|
74
|
+
for OAuth2 authentication in Streamlit applications.
|
|
75
|
+
|
|
76
|
+
.. envvar:: SWEATSTACK_CLIENT_ID
|
|
77
|
+
|
|
78
|
+
OAuth2 application client ID.
|
|
79
|
+
|
|
80
|
+
The client ID from your registered SweatStack OAuth2 application.
|
|
81
|
+
Required for Streamlit authentication if not provided to StreamlitAuth.
|
|
82
|
+
|
|
83
|
+
.. envvar:: SWEATSTACK_CLIENT_SECRET
|
|
84
|
+
|
|
85
|
+
OAuth2 application client secret.
|
|
86
|
+
|
|
87
|
+
The client secret from your registered SweatStack OAuth2 application.
|
|
88
|
+
Required for Streamlit authentication if not provided to StreamlitAuth.
|
|
89
|
+
|
|
90
|
+
.. envvar:: SWEATSTACK_SCOPES
|
|
91
|
+
|
|
92
|
+
Comma-separated list of OAuth2 scopes.
|
|
93
|
+
|
|
94
|
+
Specify which permissions to request during OAuth2 authorization.
|
|
95
|
+
|
|
96
|
+
Default: ``data:read,profile``
|
|
97
|
+
|
|
98
|
+
Example::
|
|
99
|
+
|
|
100
|
+
export SWEATSTACK_SCOPES="data:read,data:write,profile"
|
|
101
|
+
|
|
102
|
+
.. envvar:: SWEATSTACK_REDIRECT_URI
|
|
103
|
+
|
|
104
|
+
OAuth2 redirect URI.
|
|
105
|
+
|
|
106
|
+
The URI where users are redirected after OAuth2 authorization.
|
|
107
|
+
Must match a redirect URI registered in your OAuth2 application.
|
|
108
|
+
|
|
109
|
+
Example::
|
|
110
|
+
|
|
111
|
+
export SWEATSTACK_REDIRECT_URI="http://localhost:8501"
|
|
112
|
+
|
|
113
|
+
Modules
|
|
114
|
+
=======
|
|
115
|
+
|
|
116
|
+
sweatstack
|
|
117
|
+
----------
|
|
118
|
+
|
|
119
|
+
.. automodule:: sweatstack
|
|
120
|
+
:members:
|
|
121
|
+
:undoc-members:
|
|
122
|
+
:show-inheritance:
|
|
123
|
+
|
|
124
|
+
sweatstack.client
|
|
125
|
+
-----------------
|
|
126
|
+
|
|
127
|
+
.. automodule:: sweatstack.client
|
|
128
|
+
:members:
|
|
129
|
+
:undoc-members:
|
|
130
|
+
:inherited-members:
|
|
131
|
+
:show-inheritance:
|
|
132
|
+
:exclude-members: _LocalCacheMixin, _TokenStorageMixin, _OAuth2Mixin, _DelegationMixin
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
sweatstack.streamlit
|
|
136
|
+
--------------------
|
|
137
|
+
|
|
138
|
+
.. automodule:: sweatstack.streamlit
|
|
139
|
+
:members:
|
|
140
|
+
:undoc-members:
|
|
141
|
+
:show-inheritance:
|
|
142
|
+
|
|
143
|
+
sweatstack.schemas
|
|
144
|
+
------------------
|
|
145
|
+
|
|
146
|
+
.. automodule:: sweatstack.schemas
|
|
147
|
+
:members:
|
|
148
|
+
:undoc-members:
|
|
149
|
+
:show-inheritance:
|
|
150
|
+
|
|
151
|
+
Sport
|
|
152
|
+
~~~~~
|
|
153
|
+
|
|
154
|
+
.. autoclass:: sweatstack.schemas.Sport
|
|
155
|
+
:members: root_sport, parent_sport, is_sub_sport_of, is_root_sport, display_name
|
|
156
|
+
:undoc-members:
|
|
157
|
+
|
|
158
|
+
Metric
|
|
159
|
+
~~~~~~
|
|
160
|
+
|
|
161
|
+
.. autoclass:: sweatstack.schemas.Metric
|
|
162
|
+
:members: display_name
|
|
163
|
+
:undoc-members:
|
|
164
|
+
|
|
165
|
+
sweatstack.openapi_schemas
|
|
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:
|
|
256
|
+
|
|
257
|
+
.. autoclass:: sweatstack.openapi_schemas.BackfillStatus
|
|
258
|
+
:members:
|
|
259
|
+
:undoc-members:
|
|
260
|
+
:show-inheritance:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sweatstack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.56.0"
|
|
4
4
|
description = "The official Python client for SweatStack"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -33,6 +33,10 @@ build-backend = "hatchling.build"
|
|
|
33
33
|
[dependency-groups]
|
|
34
34
|
dev = [
|
|
35
35
|
"datamodel-code-generator>=0.26.5",
|
|
36
|
+
"myst-parser>=4.0.1",
|
|
37
|
+
"sphinx>=8.2.3",
|
|
38
|
+
"sphinx-markdown-builder>=0.6.8",
|
|
39
|
+
"streamlit>=1.42.0",
|
|
36
40
|
]
|
|
37
41
|
|
|
38
42
|
[tool.uv.workspace]
|
|
@@ -51,8 +51,16 @@ AUTH_SUCCESSFUL_RESPONSE = """<!DOCTYPE html>
|
|
|
51
51
|
OAUTH2_CLIENT_ID = "5382f68b0d254378"
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
class
|
|
55
|
-
"""Mixin for handling local filesystem caching of API responses.
|
|
54
|
+
class _LocalCacheMixin:
|
|
55
|
+
"""Mixin for handling local filesystem caching of API responses.
|
|
56
|
+
|
|
57
|
+
Caching is controlled via environment variables:
|
|
58
|
+
|
|
59
|
+
- :envvar:`SWEATSTACK_LOCAL_CACHE` - Enable/disable caching
|
|
60
|
+
- :envvar:`SWEATSTACK_CACHE_DIR` - Custom cache directory location
|
|
61
|
+
|
|
62
|
+
Use :meth:`clear_cache` to remove all cached data for the current user.
|
|
63
|
+
"""
|
|
56
64
|
|
|
57
65
|
def _cache_enabled(self) -> bool:
|
|
58
66
|
"""Check if local caching is enabled."""
|
|
@@ -155,7 +163,7 @@ class LocalCacheMixin:
|
|
|
155
163
|
self._log_cache_error("clear", e)
|
|
156
164
|
|
|
157
165
|
|
|
158
|
-
class
|
|
166
|
+
class _TokenStorageMixin:
|
|
159
167
|
"""Mixin for handling persistent token storage using platformdirs."""
|
|
160
168
|
|
|
161
169
|
def _get_token_file_path(self) -> Path:
|
|
@@ -200,7 +208,9 @@ except ImportError:
|
|
|
200
208
|
__version__ = "unknown"
|
|
201
209
|
|
|
202
210
|
|
|
203
|
-
class
|
|
211
|
+
class _OAuth2Mixin:
|
|
212
|
+
"""OAuth2 authentication methods for the Client class."""
|
|
213
|
+
|
|
204
214
|
def generate_pkce_params(self) -> tuple[str, str]:
|
|
205
215
|
"""Generate PKCE parameters for OAuth2 authorization.
|
|
206
216
|
|
|
@@ -436,7 +446,9 @@ class OAuth2Mixin:
|
|
|
436
446
|
self.login(persist_api_key=persist_api_key)
|
|
437
447
|
|
|
438
448
|
|
|
439
|
-
class
|
|
449
|
+
class _DelegationMixin:
|
|
450
|
+
"""User delegation methods for accessing data on behalf of other users."""
|
|
451
|
+
|
|
440
452
|
def _validate_user(self, user: str | UserSummary):
|
|
441
453
|
if isinstance(user, UserSummary):
|
|
442
454
|
return user.id
|
|
@@ -639,30 +651,61 @@ class DelegationMixin:
|
|
|
639
651
|
)
|
|
640
652
|
|
|
641
653
|
|
|
642
|
-
class Client(
|
|
654
|
+
class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixin):
|
|
655
|
+
"""SweatStack API client for accessing activities, traces, and user data.
|
|
656
|
+
|
|
657
|
+
The Client handles authentication, API requests, and data retrieval from SweatStack.
|
|
658
|
+
You can initialize it with credentials or use authenticate()/login() for OAuth2.
|
|
659
|
+
|
|
660
|
+
Example:
|
|
661
|
+
client = Client()
|
|
662
|
+
client.authenticate()
|
|
663
|
+
activities = client.get_activities(limit=10)
|
|
664
|
+
"""
|
|
665
|
+
|
|
643
666
|
def __init__(
|
|
644
667
|
self,
|
|
645
668
|
api_key: str | None = None,
|
|
646
669
|
refresh_token: str | None = None,
|
|
647
670
|
url: str | None = None,
|
|
648
671
|
streamlit_compatible: bool = False,
|
|
672
|
+
client_id: str | None = None,
|
|
673
|
+
client_secret: str | None = None,
|
|
649
674
|
):
|
|
675
|
+
"""Initialize a SweatStack client.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
api_key: Optional API access token. If not provided, will check environment or storage.
|
|
679
|
+
refresh_token: Optional refresh token for automatic token renewal.
|
|
680
|
+
url: Optional SweatStack instance URL. Defaults to production.
|
|
681
|
+
streamlit_compatible: Set to True when using in Streamlit apps.
|
|
682
|
+
"""
|
|
650
683
|
self.api_key = api_key
|
|
651
684
|
self.refresh_token = refresh_token
|
|
652
685
|
self.url = url
|
|
653
686
|
self.streamlit_compatible = streamlit_compatible
|
|
687
|
+
self.client_id = client_id or OAUTH2_CLIENT_ID
|
|
688
|
+
self.client_secret = client_secret
|
|
654
689
|
|
|
655
690
|
def _do_token_refresh(self, tz: str) -> str:
|
|
656
|
-
|
|
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:
|
|
657
699
|
response = client.post(
|
|
658
700
|
"/api/v1/oauth/token",
|
|
659
|
-
|
|
701
|
+
data={
|
|
660
702
|
"grant_type": "refresh_token",
|
|
661
|
-
"refresh_token":
|
|
703
|
+
"refresh_token": refresh_token,
|
|
662
704
|
"tz": tz,
|
|
705
|
+
"client_id": self.client_id,
|
|
706
|
+
"client_secret": self.client_secret,
|
|
663
707
|
},
|
|
664
708
|
)
|
|
665
|
-
|
|
666
709
|
self._raise_for_status(response)
|
|
667
710
|
return response.json()["access_token"]
|
|
668
711
|
|
|
@@ -670,12 +713,13 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
|
|
|
670
713
|
try:
|
|
671
714
|
body = decode_jwt_body(token)
|
|
672
715
|
# Margin in seconds to account for time to token validation of the next request
|
|
673
|
-
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.
|
|
674
717
|
if body["exp"] - TOKEN_EXPIRY_MARGIN < time.time():
|
|
675
718
|
# Token is (almost) expired, refresh it
|
|
676
719
|
token = self._do_token_refresh(body["tz"])
|
|
677
720
|
self._api_key = token
|
|
678
|
-
except Exception:
|
|
721
|
+
except Exception as exception:
|
|
722
|
+
logging.warning("Exception checking token expiry: %s", exception)
|
|
679
723
|
# If token can't be decoded, just return as-is
|
|
680
724
|
# @TODO: This probably should be handled differently
|
|
681
725
|
pass
|
|
@@ -684,6 +728,11 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
|
|
|
684
728
|
|
|
685
729
|
@property
|
|
686
730
|
def api_key(self) -> str:
|
|
731
|
+
"""The current API access token.
|
|
732
|
+
|
|
733
|
+
Automatically loads from instance, environment (SWEATSTACK_API_KEY),
|
|
734
|
+
or persistent storage. Refreshes expired tokens automatically.
|
|
735
|
+
"""
|
|
687
736
|
if self._api_key is not None:
|
|
688
737
|
value = self._api_key
|
|
689
738
|
elif value := os.getenv("SWEATSTACK_API_KEY"):
|
|
@@ -703,6 +752,10 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
|
|
|
703
752
|
|
|
704
753
|
@property
|
|
705
754
|
def refresh_token(self) -> str:
|
|
755
|
+
"""The refresh token used for automatic token renewal.
|
|
756
|
+
|
|
757
|
+
Loads from instance, environment (SWEATSTACK_REFRESH_TOKEN), or persistent storage.
|
|
758
|
+
"""
|
|
706
759
|
if self._refresh_token is not None:
|
|
707
760
|
return self._refresh_token
|
|
708
761
|
elif value := os.getenv("SWEATSTACK_REFRESH_TOKEN"):
|
|
@@ -736,16 +789,27 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
|
|
|
736
789
|
self._url = value
|
|
737
790
|
|
|
738
791
|
@contextlib.contextmanager
|
|
739
|
-
def _http_client(self):
|
|
792
|
+
def _http_client(self, skip_token_check: bool = False):
|
|
740
793
|
"""
|
|
741
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.
|
|
742
799
|
"""
|
|
743
800
|
headers = {
|
|
744
801
|
"User-Agent": f"python-sweatstack/{__version__}",
|
|
745
802
|
}
|
|
746
|
-
if
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
|
|
749
813
|
with httpx.Client(base_url=self.url, headers=headers, timeout=60) as client:
|
|
750
814
|
yield client
|
|
751
815
|
|
|
@@ -1042,6 +1106,41 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
|
|
|
1042
1106
|
df = pd.read_parquet(BytesIO(response.content))
|
|
1043
1107
|
return self._postprocess_dataframe(df)
|
|
1044
1108
|
|
|
1109
|
+
def get_activity_awd(
|
|
1110
|
+
self,
|
|
1111
|
+
activity_id: str,
|
|
1112
|
+
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"] | None = None,
|
|
1113
|
+
) -> pd.DataFrame:
|
|
1114
|
+
"""Gets the accumulated work duration (AWD) for a specific activity.
|
|
1115
|
+
|
|
1116
|
+
This method retrieves accumulated work duration metrics for a specific activity.
|
|
1117
|
+
AWD represents the total duration spent at each intensity level by sorting
|
|
1118
|
+
activity data by intensity.
|
|
1119
|
+
|
|
1120
|
+
Args:
|
|
1121
|
+
activity_id: The unique identifier of the activity.
|
|
1122
|
+
metric: Optional metric type. Defaults to power for cycling, speed for other sports.
|
|
1123
|
+
Can be either "power" or "speed".
|
|
1124
|
+
|
|
1125
|
+
Returns:
|
|
1126
|
+
pd.DataFrame: A pandas DataFrame containing the AWD data.
|
|
1127
|
+
|
|
1128
|
+
Raises:
|
|
1129
|
+
HTTPStatusError: If the API request fails.
|
|
1130
|
+
"""
|
|
1131
|
+
params = {}
|
|
1132
|
+
if metric is not None:
|
|
1133
|
+
params["metric"] = self._enums_to_strings([metric])[0]
|
|
1134
|
+
|
|
1135
|
+
with self._http_client() as client:
|
|
1136
|
+
response = client.get(
|
|
1137
|
+
url=f"/api/v1/activities/{activity_id}/accumulated-work-duration",
|
|
1138
|
+
params=params,
|
|
1139
|
+
)
|
|
1140
|
+
self._raise_for_status(response)
|
|
1141
|
+
df = pd.read_parquet(BytesIO(response.content))
|
|
1142
|
+
return self._postprocess_dataframe(df)
|
|
1143
|
+
|
|
1045
1144
|
def get_latest_activity_data(
|
|
1046
1145
|
self,
|
|
1047
1146
|
sport: Sport | str | None = None,
|
|
@@ -1215,6 +1314,57 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
|
|
|
1215
1314
|
df = pd.read_parquet(BytesIO(response.content))
|
|
1216
1315
|
return self._postprocess_dataframe(df)
|
|
1217
1316
|
|
|
1317
|
+
def get_longitudinal_awd(
|
|
1318
|
+
self,
|
|
1319
|
+
*,
|
|
1320
|
+
sport: Sport | str,
|
|
1321
|
+
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1322
|
+
date: date | str | None = None,
|
|
1323
|
+
window_days: int | None = None,
|
|
1324
|
+
) -> pd.DataFrame:
|
|
1325
|
+
"""Gets the longitudinal accumulated work duration (AWD) for a specific sport and metric.
|
|
1326
|
+
|
|
1327
|
+
This method retrieves AWD values across four intensity levels: max (highest daily AWD),
|
|
1328
|
+
hard, medium, and easy (sustainable durations for respective workout intensities).
|
|
1329
|
+
|
|
1330
|
+
Note: This endpoint is in development and subject to change.
|
|
1331
|
+
|
|
1332
|
+
Args:
|
|
1333
|
+
sport: The sport to get AWD data for. Can be a Sport enum or string ID.
|
|
1334
|
+
metric: The metric to calculate AWD for. Must be either "power" or "speed".
|
|
1335
|
+
date: Optional reference date for the AWD calculation. If provided,
|
|
1336
|
+
the AWD will be calculated up to this date. Can be a date object
|
|
1337
|
+
or string in ISO format.
|
|
1338
|
+
window_days: Optional number of days to include in the calculation window
|
|
1339
|
+
before the reference date. If None, all available data is used.
|
|
1340
|
+
|
|
1341
|
+
Returns:
|
|
1342
|
+
pd.DataFrame: A pandas DataFrame containing the longitudinal AWD data with intensity levels.
|
|
1343
|
+
|
|
1344
|
+
Raises:
|
|
1345
|
+
HTTPStatusError: If the API request fails.
|
|
1346
|
+
"""
|
|
1347
|
+
sport = self._enums_to_strings([sport])[0]
|
|
1348
|
+
metric = self._enums_to_strings([metric])[0]
|
|
1349
|
+
|
|
1350
|
+
params = {
|
|
1351
|
+
"sport": sport,
|
|
1352
|
+
"metric": metric,
|
|
1353
|
+
}
|
|
1354
|
+
if date is not None:
|
|
1355
|
+
params["date"] = date
|
|
1356
|
+
if window_days is not None:
|
|
1357
|
+
params["window_days"] = window_days
|
|
1358
|
+
|
|
1359
|
+
with self._http_client() as client:
|
|
1360
|
+
response = client.get(
|
|
1361
|
+
url="/api/v1/activities/longitudinal-accumulated-work-duration",
|
|
1362
|
+
params=params,
|
|
1363
|
+
)
|
|
1364
|
+
self._raise_for_status(response)
|
|
1365
|
+
df = pd.read_parquet(BytesIO(response.content))
|
|
1366
|
+
return self._postprocess_dataframe(df)
|
|
1367
|
+
|
|
1218
1368
|
def _get_traces_generator(
|
|
1219
1369
|
self,
|
|
1220
1370
|
*,
|
|
@@ -1621,6 +1771,7 @@ _generate_singleton_methods(
|
|
|
1621
1771
|
"get_activity",
|
|
1622
1772
|
"get_activity_data",
|
|
1623
1773
|
"get_activity_mean_max",
|
|
1774
|
+
"get_activity_awd",
|
|
1624
1775
|
|
|
1625
1776
|
"get_latest_activity",
|
|
1626
1777
|
"get_latest_activity_data",
|
|
@@ -1628,6 +1779,7 @@ _generate_singleton_methods(
|
|
|
1628
1779
|
|
|
1629
1780
|
"get_longitudinal_data",
|
|
1630
1781
|
"get_longitudinal_mean_max",
|
|
1782
|
+
"get_longitudinal_awd",
|
|
1631
1783
|
|
|
1632
1784
|
"get_traces",
|
|
1633
1785
|
"create_trace",
|
|
File without changes
|