sweatstack 0.34.0__tar.gz → 0.35.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.34.0 → sweatstack-0.35.0}/PKG-INFO +1 -1
- {sweatstack-0.34.0 → sweatstack-0.35.0}/pyproject.toml +1 -1
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/client.py +316 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/streamlit.py +104 -2
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/utils.py +12 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/.gitignore +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/.python-version +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/Makefile +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/README.md +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/playground/README.md +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/playground/Untitled.ipynb +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/playground/hello.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/playground/pyproject.toml +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.34.0 → sweatstack-0.35.0}/uv.lock +0 -0
|
@@ -2,6 +2,7 @@ import base64
|
|
|
2
2
|
import contextlib
|
|
3
3
|
import random
|
|
4
4
|
import hashlib
|
|
5
|
+
import logging
|
|
5
6
|
import os
|
|
6
7
|
import secrets
|
|
7
8
|
import time
|
|
@@ -52,6 +53,26 @@ except ImportError:
|
|
|
52
53
|
|
|
53
54
|
class OAuth2Mixin:
|
|
54
55
|
def login(self):
|
|
56
|
+
"""Initiates the OAuth2 login flow for SweatStack authentication.
|
|
57
|
+
|
|
58
|
+
This method starts a local HTTP server to receive the OAuth2 callback,
|
|
59
|
+
opens a browser window for the user to authenticate with SweatStack,
|
|
60
|
+
and exchanges the authorization code for an access token.
|
|
61
|
+
|
|
62
|
+
The method uses PKCE (Proof Key for Code Exchange) for enhanced security
|
|
63
|
+
during the OAuth2 authorization code flow.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
None
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
Exception: If the authentication process times out or fails.
|
|
70
|
+
|
|
71
|
+
Note:
|
|
72
|
+
This method requires a working internet connection and the ability
|
|
73
|
+
to open a browser window. It will also temporarily open a local HTTP
|
|
74
|
+
server on a random port between 8000-9000.
|
|
75
|
+
"""
|
|
55
76
|
class AuthHandler(BaseHTTPRequestHandler):
|
|
56
77
|
def log_message(self, format, *args):
|
|
57
78
|
# This override disables logging.
|
|
@@ -147,6 +168,20 @@ class DelegationMixin:
|
|
|
147
168
|
return response.json()
|
|
148
169
|
|
|
149
170
|
def switch_user(self, user: str | UserSummary):
|
|
171
|
+
"""Switches the client to operate on behalf of another user.
|
|
172
|
+
|
|
173
|
+
This method changes the current client's authentication to act on behalf of the specified user.
|
|
174
|
+
The client will use a delegated token for all subsequent API calls.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
user: Either a UserSummary object or a string user ID representing the user to switch to.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
None
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
HTTPStatusError: If the delegation request fails.
|
|
184
|
+
"""
|
|
150
185
|
token_response = self._get_delegated_token(user)
|
|
151
186
|
self.api_key = token_response["access_token"]
|
|
152
187
|
self.refresh_token = token_response["refresh_token"]
|
|
@@ -160,11 +195,37 @@ class DelegationMixin:
|
|
|
160
195
|
return response.json()
|
|
161
196
|
|
|
162
197
|
def switch_back(self):
|
|
198
|
+
"""Switches the client back to the principal user.
|
|
199
|
+
|
|
200
|
+
This method reverts the client's authentication from a delegated user back to the principal user.
|
|
201
|
+
The client will use the principal token for all subsequent API calls.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
None
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
HTTPStatusError: If the principal token request fails.
|
|
208
|
+
"""
|
|
209
|
+
|
|
163
210
|
token_response = self._get_principal_token()
|
|
164
211
|
self.api_key = token_response["access_token"]
|
|
165
212
|
self.refresh_token = token_response["refresh_token"]
|
|
166
213
|
|
|
167
214
|
def delegated_client(self, user: str | UserSummary):
|
|
215
|
+
"""Creates a new client instance that operates on behalf of another user.
|
|
216
|
+
|
|
217
|
+
This method creates a new client instance with delegated authentication for the specified user.
|
|
218
|
+
Unlike `switch_user`, this method does not modify the current client but returns a new one.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
user: Either a UserSummary object or a string user ID representing the user to delegate to.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Client: A new client instance authenticated as the delegated user.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
HTTPStatusError: If the delegation request fails.
|
|
228
|
+
"""
|
|
168
229
|
token_response = self._get_delegated_token(user)
|
|
169
230
|
return self.__class__(
|
|
170
231
|
api_key=token_response["access_token"],
|
|
@@ -174,6 +235,17 @@ class DelegationMixin:
|
|
|
174
235
|
)
|
|
175
236
|
|
|
176
237
|
def principal_client(self):
|
|
238
|
+
"""Creates a new client instance that operates as the principal user.
|
|
239
|
+
|
|
240
|
+
This method creates a new client instance with authentication for the principal user.
|
|
241
|
+
Unlike `switch_back`, this method does not modify the current client but returns a new one.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Client: A new client instance authenticated as the principal user.
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
HTTPStatusError: If the principal token request fails.
|
|
248
|
+
"""
|
|
177
249
|
token_response = self._get_principal_token()
|
|
178
250
|
return self.__class__(
|
|
179
251
|
api_key=token_response["access_token"],
|
|
@@ -290,6 +362,23 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
290
362
|
def _raise_for_status(self, response: httpx.Response):
|
|
291
363
|
if response.status_code == 422:
|
|
292
364
|
raise ValueError(response.json())
|
|
365
|
+
elif response.status_code == 401:
|
|
366
|
+
try:
|
|
367
|
+
import streamlit
|
|
368
|
+
except ImportError:
|
|
369
|
+
response.raise_for_status()
|
|
370
|
+
else:
|
|
371
|
+
try:
|
|
372
|
+
response.raise_for_status()
|
|
373
|
+
except Exception as exception:
|
|
374
|
+
if not self.streamlit_compatible:
|
|
375
|
+
streamlit_error_message = (
|
|
376
|
+
"\nStreamlit environment detected. Use StreamlitAuth.client instance.\n"
|
|
377
|
+
"Docs: https://developer.sweatstack.no/learn/integrations/streamlit/"
|
|
378
|
+
)
|
|
379
|
+
exception.add_note(streamlit_error_message)
|
|
380
|
+
raise
|
|
381
|
+
|
|
293
382
|
else:
|
|
294
383
|
response.raise_for_status()
|
|
295
384
|
|
|
@@ -356,6 +445,23 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
356
445
|
limit: int = 100,
|
|
357
446
|
as_dataframe: bool = False,
|
|
358
447
|
) -> Generator[ActivitySummary, None, None] | pd.DataFrame:
|
|
448
|
+
"""Gets a list of activities based on specified filters.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
start: Optional start date to filter activities.
|
|
452
|
+
end: Optional end date to filter activities.
|
|
453
|
+
sports: Optional list of sports to filter activities by. Can be Sport objects or string IDs.
|
|
454
|
+
tags: Optional list of tags to filter activities by.
|
|
455
|
+
limit: Maximum number of activities to return. Defaults to 100.
|
|
456
|
+
as_dataframe: Whether to return results as a pandas DataFrame. Defaults to False.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Either a generator yielding ActivitySummary objects or a pandas DataFrame containing
|
|
460
|
+
the activities data, depending on the value of as_dataframe.
|
|
461
|
+
|
|
462
|
+
Raises:
|
|
463
|
+
HTTPStatusError: If the API request fails.
|
|
464
|
+
"""
|
|
359
465
|
generator = self._get_activities_generator(
|
|
360
466
|
start=start,
|
|
361
467
|
end=end,
|
|
@@ -381,6 +487,21 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
381
487
|
sport: Sport | None = None,
|
|
382
488
|
tag: str | None = None,
|
|
383
489
|
) -> ActivityDetails:
|
|
490
|
+
"""Gets the most recent activity based on specified filters.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
start: Optional start date to filter activities.
|
|
494
|
+
end: Optional end date to filter activities.
|
|
495
|
+
sport: Optional sport to filter activities by. Can be a Sport object or string ID.
|
|
496
|
+
tag: Optional tag to filter activities by.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
ActivityDetails: The most recent activity matching the filters.
|
|
500
|
+
|
|
501
|
+
Raises:
|
|
502
|
+
StopIteration: If no activities match the filters.
|
|
503
|
+
HTTPStatusError: If the API request fails.
|
|
504
|
+
"""
|
|
384
505
|
return next(self.get_activities(
|
|
385
506
|
start=start,
|
|
386
507
|
end=end,
|
|
@@ -390,6 +511,17 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
390
511
|
))
|
|
391
512
|
|
|
392
513
|
def get_activity(self, activity_id: str) -> ActivityDetails:
|
|
514
|
+
"""Gets details for a specific activity by ID.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
activity_id: The unique identifier of the activity to retrieve.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
ActivityDetails: The activity details object containing all information about the activity.
|
|
521
|
+
|
|
522
|
+
Raises:
|
|
523
|
+
HTTPStatusError: If the API request fails.
|
|
524
|
+
"""
|
|
393
525
|
with self._http_client() as client:
|
|
394
526
|
response = client.get(url=f"/api/v1/activities/{activity_id}")
|
|
395
527
|
self._raise_for_status(response)
|
|
@@ -400,6 +532,22 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
400
532
|
activity_id: str,
|
|
401
533
|
adaptive_sampling_on: Literal["power", "speed"] | None = None,
|
|
402
534
|
) -> pd.DataFrame:
|
|
535
|
+
"""Gets the raw data for a specific activity.
|
|
536
|
+
|
|
537
|
+
This method retrieves the time-series data for a given activity, with optional
|
|
538
|
+
adaptive sampling to reduce data points for visualization.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
activity_id: The unique identifier of the activity.
|
|
542
|
+
adaptive_sampling_on: Optional parameter to apply adaptive sampling on
|
|
543
|
+
either "power" or "speed" data. If None, no adaptive sampling is applied.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
pd.DataFrame: A pandas DataFrame containing the activity's time-series data.
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
HTTPStatusError: If the API request fails.
|
|
550
|
+
"""
|
|
403
551
|
params = {}
|
|
404
552
|
if adaptive_sampling_on is not None:
|
|
405
553
|
params["adaptive_sampling_on"] = adaptive_sampling_on
|
|
@@ -420,6 +568,23 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
420
568
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
421
569
|
adaptive_sampling: bool = False,
|
|
422
570
|
) -> pd.DataFrame:
|
|
571
|
+
"""Gets the mean-max data for a specific activity.
|
|
572
|
+
|
|
573
|
+
This method retrieves the mean-max curve data for a given activity, which represents
|
|
574
|
+
the maximum average value of a metric (power or speed) for different time durations.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
activity_id: The unique identifier of the activity.
|
|
578
|
+
metric: The metric to calculate mean-max values for, either "power" or "speed".
|
|
579
|
+
adaptive_sampling: Whether to apply adaptive sampling to reduce data points
|
|
580
|
+
for visualization. Defaults to False.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
pd.DataFrame: A pandas DataFrame containing the mean-max curve data.
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
HTTPStatusError: If the API request fails.
|
|
587
|
+
"""
|
|
423
588
|
metric = self._enums_to_strings([metric])[0]
|
|
424
589
|
with self._http_client() as client:
|
|
425
590
|
response = client.get(
|
|
@@ -438,6 +603,22 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
438
603
|
sport: Sport | str | None = None,
|
|
439
604
|
adaptive_sampling_on: Literal["power", "speed"] | None = None,
|
|
440
605
|
) -> pd.DataFrame:
|
|
606
|
+
"""Gets the data for the latest activity of a specific sport.
|
|
607
|
+
|
|
608
|
+
This method retrieves the time series data for the most recent activity of the specified sport.
|
|
609
|
+
If no sport is specified, it returns data for the latest activity regardless of sport.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
sport: Optional sport to filter by. Can be a Sport enum or string.
|
|
613
|
+
adaptive_sampling_on: Optional metric to apply adaptive sampling for visualization.
|
|
614
|
+
Can be either "power" or "speed". Defaults to None.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
pd.DataFrame: A pandas DataFrame containing the activity data.
|
|
618
|
+
|
|
619
|
+
Raises:
|
|
620
|
+
HTTPStatusError: If the API request fails.
|
|
621
|
+
"""
|
|
441
622
|
activity = self.get_latest_activity(sport=sport)
|
|
442
623
|
return self.get_activity_data(activity.id, adaptive_sampling_on)
|
|
443
624
|
|
|
@@ -447,6 +628,23 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
447
628
|
sport: Sport | str | None = None,
|
|
448
629
|
adaptive_sampling: bool = False,
|
|
449
630
|
) -> pd.DataFrame:
|
|
631
|
+
"""Gets the mean-max curve for the latest activity of a specific sport.
|
|
632
|
+
|
|
633
|
+
This method retrieves the mean-max curve data for the most recent activity of the specified sport.
|
|
634
|
+
If no sport is specified, it returns data for the latest activity regardless of sport.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
metric: The metric to calculate the mean-max curve for. Can be either "power" or "speed".
|
|
638
|
+
sport: Optional sport to filter by. Can be a Sport enum or string.
|
|
639
|
+
adaptive_sampling: Whether to apply adaptive sampling to the mean-max curve data.
|
|
640
|
+
Defaults to False.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
pd.DataFrame: A pandas DataFrame containing the mean-max curve data.
|
|
644
|
+
|
|
645
|
+
Raises:
|
|
646
|
+
HTTPStatusError: If the API request fails.
|
|
647
|
+
"""
|
|
450
648
|
activity = self.get_latest_activity(sport=sport)
|
|
451
649
|
return self.get_activity_mean_max(activity.id, metric, adaptive_sampling)
|
|
452
650
|
|
|
@@ -460,6 +658,29 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
460
658
|
metrics: list[Metric | str] | None = None,
|
|
461
659
|
adaptive_sampling_on: Literal[Metric.power, Metric.speed] | Literal["power", "speed"] | None = None,
|
|
462
660
|
) -> pd.DataFrame:
|
|
661
|
+
"""Gets longitudinal data for activities within a specified date range.
|
|
662
|
+
|
|
663
|
+
This method retrieves aggregated data for activities that match the specified criteria,
|
|
664
|
+
including sport type and date range. The data is returned as a pandas DataFrame.
|
|
665
|
+
|
|
666
|
+
Args:
|
|
667
|
+
sport: Optional single sport to filter by. Can be a Sport enum or string.
|
|
668
|
+
Cannot be used together with 'sports'.
|
|
669
|
+
sports: Optional list of sports to filter by. Can be a list of Sport enums or strings.
|
|
670
|
+
Cannot be used together with 'sport'.
|
|
671
|
+
start: The start date for the data range. Can be a date object or string in ISO format.
|
|
672
|
+
end: Optional end date for the data range. Can be a date object or string in ISO format.
|
|
673
|
+
metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
|
|
674
|
+
adaptive_sampling_on: Optional metric to apply adaptive sampling for visualization.
|
|
675
|
+
Can be either "power" or "speed". Defaults to None.
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
pd.DataFrame: A pandas DataFrame containing the longitudinal activity data.
|
|
679
|
+
|
|
680
|
+
Raises:
|
|
681
|
+
ValueError: If both 'sport' and 'sports' parameters are provided.
|
|
682
|
+
HTTPStatusError: If the API request fails.
|
|
683
|
+
"""
|
|
463
684
|
if sport and sports:
|
|
464
685
|
raise ValueError("Cannot specify both sport and sports")
|
|
465
686
|
if sport is not None:
|
|
@@ -496,6 +717,26 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
496
717
|
date: date | str | None = None,
|
|
497
718
|
window_days: int | None = None,
|
|
498
719
|
) -> pd.DataFrame:
|
|
720
|
+
"""Gets the mean-max curve for a specific sport and metric.
|
|
721
|
+
|
|
722
|
+
This method retrieves the mean-max curve data for a given sport and metric,
|
|
723
|
+
optionally filtered by date and window size.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
sport: The sport to get mean-max data for. Can be a Sport enum or string ID.
|
|
727
|
+
metric: The metric to calculate mean-max for. Must be either "power" or "speed".
|
|
728
|
+
date: Optional reference date for the mean-max calculation. If provided,
|
|
729
|
+
the mean-max curve will be calculated up to this date. Can be a date object
|
|
730
|
+
or string in ISO format.
|
|
731
|
+
window_days: Optional number of days to include in the calculation window
|
|
732
|
+
before the reference date. If None, all available data is used.
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
pd.DataFrame: A pandas DataFrame containing the mean-max curve data.
|
|
736
|
+
|
|
737
|
+
Raises:
|
|
738
|
+
HTTPStatusError: If the API request fails.
|
|
739
|
+
"""
|
|
499
740
|
sport = self._enums_to_strings([sport])[0]
|
|
500
741
|
metric = self._enums_to_strings([metric])[0]
|
|
501
742
|
|
|
@@ -601,6 +842,23 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
601
842
|
limit: int = 100,
|
|
602
843
|
as_dataframe: bool = False,
|
|
603
844
|
) -> Generator[TraceDetails, None, None] | pd.DataFrame:
|
|
845
|
+
"""Gets a list of traces based on specified filters.
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
start: Optional start date to filter traces.
|
|
849
|
+
end: Optional end date to filter traces.
|
|
850
|
+
sports: Optional list of sports to filter traces by. Can be Sport objects or string IDs.
|
|
851
|
+
tags: Optional list of tags to filter traces by.
|
|
852
|
+
limit: Maximum number of traces to return. Defaults to 100.
|
|
853
|
+
as_dataframe: Whether to return results as a pandas DataFrame. Defaults to False.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
Either a generator yielding TraceDetails objects or a pandas DataFrame containing
|
|
857
|
+
the traces data, depending on the value of as_dataframe.
|
|
858
|
+
|
|
859
|
+
Raises:
|
|
860
|
+
HTTPStatusError: If the API request fails.
|
|
861
|
+
"""
|
|
604
862
|
generator = self._get_traces_generator(
|
|
605
863
|
start=start,
|
|
606
864
|
end=end,
|
|
@@ -633,6 +891,27 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
633
891
|
heart_rate: int | None = None,
|
|
634
892
|
tags: list[str] | None = None,
|
|
635
893
|
) -> TraceDetails:
|
|
894
|
+
"""Creates a new trace with the specified parameters.
|
|
895
|
+
|
|
896
|
+
This method creates a new trace entry with the given timestamp and optional
|
|
897
|
+
measurement values.
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
timestamp: The date and time when the trace was recorded.
|
|
901
|
+
lactate: Optional blood lactate concentration in mmol/L.
|
|
902
|
+
rpe: Optional rating of perceived exertion (typically on a scale of 1-10).
|
|
903
|
+
notes: Optional text notes associated with this trace.
|
|
904
|
+
power: Optional power measurement in watts.
|
|
905
|
+
speed: Optional speed measurement in meters per second.
|
|
906
|
+
heart_rate: Optional heart rate measurement in beats per minute.
|
|
907
|
+
tags: Optional list of tags to associate with this trace.
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
TraceDetails: The created trace object with all details.
|
|
911
|
+
|
|
912
|
+
Raises:
|
|
913
|
+
HTTPStatusError: If the API request fails.
|
|
914
|
+
"""
|
|
636
915
|
with self._http_client() as client:
|
|
637
916
|
response = client.post(
|
|
638
917
|
url="/api/v1/traces/",
|
|
@@ -651,6 +930,20 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
651
930
|
return TraceDetails.model_validate(response.json())
|
|
652
931
|
|
|
653
932
|
def get_sports(self, only_root: bool = False) -> list[Sport]:
|
|
933
|
+
"""Gets a list of available sports.
|
|
934
|
+
|
|
935
|
+
This method retrieves all sports available to the user, with an option to only
|
|
936
|
+
return root sports (top-level sports without parents).
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
only_root: If True, only returns root sports without parents. Defaults to False.
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
list[Sport]: A list of Sport objects representing the available sports.
|
|
943
|
+
|
|
944
|
+
Raises:
|
|
945
|
+
HTTPStatusError: If the API request fails.
|
|
946
|
+
"""
|
|
654
947
|
with self._http_client() as client:
|
|
655
948
|
response = client.get(
|
|
656
949
|
url="/api/v1/profile/sports/",
|
|
@@ -660,6 +953,17 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
660
953
|
return [Sport(sport) for sport in response.json()]
|
|
661
954
|
|
|
662
955
|
def get_tags(self) -> list[str]:
|
|
956
|
+
"""Gets a list of all tags used by the user.
|
|
957
|
+
|
|
958
|
+
This method retrieves all tags that the user has created or used across
|
|
959
|
+
their activities and traces.
|
|
960
|
+
|
|
961
|
+
Returns:
|
|
962
|
+
list[str]: A list of tag strings.
|
|
963
|
+
|
|
964
|
+
Raises:
|
|
965
|
+
HTTPStatusError: If the API request fails.
|
|
966
|
+
"""
|
|
663
967
|
with self._http_client() as client:
|
|
664
968
|
response = client.get(
|
|
665
969
|
url="/api/v1/profile/tags/",
|
|
@@ -668,6 +972,18 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
668
972
|
return response.json()
|
|
669
973
|
|
|
670
974
|
def get_users(self) -> list[UserSummary]:
|
|
975
|
+
"""Gets a list of all users accessible to the current user.
|
|
976
|
+
|
|
977
|
+
This method retrieves all users that the current user has access to view.
|
|
978
|
+
For regular users, this typically returns only their own user information.
|
|
979
|
+
For admin users, this may return information about multiple users.
|
|
980
|
+
|
|
981
|
+
Returns:
|
|
982
|
+
list[UserSummary]: A list of UserSummary objects containing basic user information.
|
|
983
|
+
|
|
984
|
+
Raises:
|
|
985
|
+
HTTPStatusError: If the API request fails.
|
|
986
|
+
"""
|
|
671
987
|
with self._http_client() as client:
|
|
672
988
|
response = client.get(
|
|
673
989
|
url="/api/v1/users/",
|
|
@@ -118,9 +118,31 @@ class StreamlitAuth:
|
|
|
118
118
|
return
|
|
119
119
|
|
|
120
120
|
def is_authenticated(self):
|
|
121
|
+
"""Checks if the user is currently authenticated with SweatStack.
|
|
122
|
+
|
|
123
|
+
This method determines if the user has a valid API key stored in the session state
|
|
124
|
+
or in the instance. It does not verify if the API key is still valid with the server.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
bool: True if the user has an API key, False otherwise.
|
|
128
|
+
"""
|
|
121
129
|
return self.api_key is not None
|
|
122
130
|
|
|
123
131
|
def authenticate(self):
|
|
132
|
+
"""Authenticates the user with SweatStack.
|
|
133
|
+
|
|
134
|
+
This method handles the authentication flow for SweatStack in a Streamlit app.
|
|
135
|
+
It checks if the user is already authenticated, and if not, displays a login button.
|
|
136
|
+
If the user is authenticated, it displays a logout button.
|
|
137
|
+
|
|
138
|
+
When the user clicks the login button, they are redirected to the SweatStack
|
|
139
|
+
authorization page. After successful authorization, they are redirected back
|
|
140
|
+
to the Streamlit app with an authorization code, which is exchanged for an
|
|
141
|
+
access token.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
None
|
|
145
|
+
"""
|
|
124
146
|
if self.is_authenticated():
|
|
125
147
|
if not st.session_state.get("sweatstack_auth_toast_shown", False):
|
|
126
148
|
st.toast("SweatStack authentication successful!", icon="✅")
|
|
@@ -134,6 +156,20 @@ class StreamlitAuth:
|
|
|
134
156
|
self._show_sweatstack_login()
|
|
135
157
|
|
|
136
158
|
def select_user(self):
|
|
159
|
+
"""Displays a user selection dropdown and switches the client to the selected user.
|
|
160
|
+
|
|
161
|
+
This method retrieves a list of users accessible to the current user and displays
|
|
162
|
+
them in a dropdown. When a user is selected, the client is switched to operate on
|
|
163
|
+
behalf of that user. The method first switches back to the principal user to ensure
|
|
164
|
+
the full list of available users is displayed.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
UserSummary: The selected user object.
|
|
168
|
+
|
|
169
|
+
Note:
|
|
170
|
+
This method requires the user to have appropriate permissions to access other users.
|
|
171
|
+
For regular users, this typically only shows their own user information.
|
|
172
|
+
"""
|
|
137
173
|
self.switch_to_principal_user()
|
|
138
174
|
other_users = self.client.get_users()
|
|
139
175
|
selected_user = st.selectbox(
|
|
@@ -147,6 +183,18 @@ class StreamlitAuth:
|
|
|
147
183
|
return selected_user
|
|
148
184
|
|
|
149
185
|
def switch_to_principal_user(self):
|
|
186
|
+
"""Switches the client back to the principal user.
|
|
187
|
+
|
|
188
|
+
This method reverts the client's authentication from a delegated user back to the principal user.
|
|
189
|
+
The client will use the principal token for all subsequent API calls and updates the session state
|
|
190
|
+
with the new API key.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
None
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
HTTPStatusError: If the principal token request fails.
|
|
197
|
+
"""
|
|
150
198
|
self.client.switch_back()
|
|
151
199
|
self._set_api_key(self.client.api_key)
|
|
152
200
|
|
|
@@ -159,8 +207,23 @@ class StreamlitAuth:
|
|
|
159
207
|
tags: list[str] | None = None,
|
|
160
208
|
limit: int | None = 100,
|
|
161
209
|
):
|
|
162
|
-
"""
|
|
163
|
-
|
|
210
|
+
"""Select an activity from the user's activities.
|
|
211
|
+
|
|
212
|
+
This method retrieves activities based on specified filters and displays them in a
|
|
213
|
+
dropdown for selection.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
start: Optional start date to filter activities.
|
|
217
|
+
end: Optional end date to filter activities.
|
|
218
|
+
sports: Optional list of sports to filter activities by.
|
|
219
|
+
tags: Optional list of tags to filter activities by.
|
|
220
|
+
limit: Maximum number of activities to retrieve. Defaults to 100.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
The selected activity object.
|
|
224
|
+
|
|
225
|
+
Note:
|
|
226
|
+
Activities are displayed in the format "YYYY-MM-DD sport_name".
|
|
164
227
|
"""
|
|
165
228
|
|
|
166
229
|
activities = self.client.get_activities(
|
|
@@ -178,6 +241,22 @@ class StreamlitAuth:
|
|
|
178
241
|
return selected_activity
|
|
179
242
|
|
|
180
243
|
def select_sport(self, only_root: bool = False, allow_multiple: bool = False, only_available: bool = True):
|
|
244
|
+
"""Select a sport from the available sports.
|
|
245
|
+
|
|
246
|
+
This method retrieves sports and displays them in a dropdown or multiselect for selection.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
only_root: If True, only returns root sports without parents. Defaults to False.
|
|
250
|
+
allow_multiple: If True, allows selecting multiple sports. Defaults to False.
|
|
251
|
+
only_available: If True, only shows sports available to the user. If False, shows all
|
|
252
|
+
sports defined in the Sport enum. Defaults to True.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Sport or list[Sport]: The selected sport or list of sports, depending on allow_multiple.
|
|
256
|
+
|
|
257
|
+
Note:
|
|
258
|
+
Sports are displayed in a human-readable format using the format_sport function.
|
|
259
|
+
"""
|
|
181
260
|
if only_available:
|
|
182
261
|
sports = self.client.get_sports(only_root)
|
|
183
262
|
else:
|
|
@@ -201,6 +280,19 @@ class StreamlitAuth:
|
|
|
201
280
|
return selected_sport
|
|
202
281
|
|
|
203
282
|
def select_tag(self, allow_multiple: bool = False):
|
|
283
|
+
"""Select a tag from the available tags.
|
|
284
|
+
|
|
285
|
+
This method retrieves tags and displays them in a dropdown or multiselect for selection.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
allow_multiple: If True, allows selecting multiple tags. Defaults to False.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
str or list[str]: The selected tag or list of tags, depending on allow_multiple.
|
|
292
|
+
|
|
293
|
+
Note:
|
|
294
|
+
Empty tags are displayed as "-" in the dropdown.
|
|
295
|
+
"""
|
|
204
296
|
tags = self.client.get_tags()
|
|
205
297
|
if allow_multiple:
|
|
206
298
|
selected_tag = st.multiselect(
|
|
@@ -216,6 +308,16 @@ class StreamlitAuth:
|
|
|
216
308
|
return selected_tag
|
|
217
309
|
|
|
218
310
|
def select_metric(self, allow_multiple: bool = False):
|
|
311
|
+
"""Select a metric from the available metrics.
|
|
312
|
+
|
|
313
|
+
This method displays metrics in a dropdown or multiselect for selection.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
allow_multiple: If True, allows selecting multiple metrics. Defaults to False.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Metric or list[Metric]: The selected metric or list of metrics, depending on allow_multiple.
|
|
320
|
+
"""
|
|
219
321
|
if allow_multiple:
|
|
220
322
|
selected_metric = st.multiselect(
|
|
221
323
|
"Select metrics",
|
|
@@ -56,6 +56,18 @@ def make_dataframe_streamlit_compatible(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def format_sport(sport: Sport):
|
|
59
|
+
"""Formats a sport enum value into a human-readable string.
|
|
60
|
+
|
|
61
|
+
This function takes a Sport enum and converts it to a formatted string representation.
|
|
62
|
+
For example, "cycling.road" becomes "cycling (road)" and "running" remains "running".
|
|
63
|
+
Underscores in sport names are replaced with spaces.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
sport: A Sport enum value to format.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: A human-readable formatted string representation of the sport.
|
|
70
|
+
"""
|
|
59
71
|
parts = sport.value.split(".")
|
|
60
72
|
base_sport = parts[0]
|
|
61
73
|
base_sport = base_sport.replace("_", " ")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.34.0 → sweatstack-0.35.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{sweatstack-0.34.0 → sweatstack-0.35.0}/playground/Sweat Stack examples/Getting started.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.34.0 → sweatstack-0.35.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|