sweatstack 0.33.0__py3-none-any.whl → 0.35.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
@@ -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
 
@@ -458,8 +656,31 @@ class Client(OAuth2Mixin, DelegationMixin):
458
656
  start: date | str,
459
657
  end: date | str | None = None,
460
658
  metrics: list[Metric | str] | None = None,
461
- adaptive_sampling_on: Literal["power", "speed"] | None = None,
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:
@@ -467,19 +688,16 @@ class Client(OAuth2Mixin, DelegationMixin):
467
688
  elif sports is None:
468
689
  sports = []
469
690
 
470
- sports = self._enums_to_strings(sports)
471
- metrics = self._enums_to_strings(metrics)
472
-
473
691
  params = {
474
- "sports": sports,
475
- "start": start,
692
+ "sports": self._enums_to_strings(sports),
693
+ "start": start
476
694
  }
477
695
  if end is not None:
478
696
  params["end"] = end
479
697
  if metrics is not None:
480
- params["metrics"] = metrics
698
+ params["metrics"] = self._enums_to_strings(metrics)
481
699
  if adaptive_sampling_on is not None:
482
- params["adaptive_sampling_on"] = adaptive_sampling_on
700
+ params["adaptive_sampling_on"] = self._enums_to_strings([adaptive_sampling_on])[0]
483
701
 
484
702
  with self._http_client() as client:
485
703
  response = client.get(
@@ -499,6 +717,26 @@ class Client(OAuth2Mixin, DelegationMixin):
499
717
  date: date | str | None = None,
500
718
  window_days: int | None = None,
501
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
+ """
502
740
  sport = self._enums_to_strings([sport])[0]
503
741
  metric = self._enums_to_strings([metric])[0]
504
742
 
@@ -604,6 +842,23 @@ class Client(OAuth2Mixin, DelegationMixin):
604
842
  limit: int = 100,
605
843
  as_dataframe: bool = False,
606
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
+ """
607
862
  generator = self._get_traces_generator(
608
863
  start=start,
609
864
  end=end,
@@ -636,6 +891,27 @@ class Client(OAuth2Mixin, DelegationMixin):
636
891
  heart_rate: int | None = None,
637
892
  tags: list[str] | None = None,
638
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
+ """
639
915
  with self._http_client() as client:
640
916
  response = client.post(
641
917
  url="/api/v1/traces/",
@@ -654,6 +930,20 @@ class Client(OAuth2Mixin, DelegationMixin):
654
930
  return TraceDetails.model_validate(response.json())
655
931
 
656
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
+ """
657
947
  with self._http_client() as client:
658
948
  response = client.get(
659
949
  url="/api/v1/profile/sports/",
@@ -663,6 +953,17 @@ class Client(OAuth2Mixin, DelegationMixin):
663
953
  return [Sport(sport) for sport in response.json()]
664
954
 
665
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
+ """
666
967
  with self._http_client() as client:
667
968
  response = client.get(
668
969
  url="/api/v1/profile/tags/",
@@ -671,6 +972,18 @@ class Client(OAuth2Mixin, DelegationMixin):
671
972
  return response.json()
672
973
 
673
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
+ """
674
987
  with self._http_client() as client:
675
988
  response = client.get(
676
989
  url="/api/v1/users/",
sweatstack/streamlit.py CHANGED
@@ -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
- Select an activity from the user's activities.
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",
sweatstack/utils.py CHANGED
@@ -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("_", " ")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.33.0
3
+ Version: 0.35.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=NpZjqDION-cets4B87brCL3sFMBAuNiP9_HKc9l6Cy4,24898
3
+ sweatstack/client.py,sha256=t6UeZxP8-aiZ8K35G0LDqd0vuAf59WSskccQ-lDX-RQ,38340
4
4
  sweatstack/constants.py,sha256=fGO6ksOv5HeISv9lHRoYm4besW1GTveXS8YD3K0ljg0,41
5
5
  sweatstack/ipython_init.py,sha256=zBGUlMFkdpLvsNpOpwrNaKRUpUZhzaICvH8ODJgMPcI,229
6
6
  sweatstack/jupyterlab_oauth2_startup.py,sha256=eZ6xi0Sa4hO4vUanimq0SqjduHtiywCURSDNWk_I-7s,1200
7
7
  sweatstack/openapi_schemas.py,sha256=XlgiL7qkfcfoDHcQrIm9e5hvhY98onC0QmZWG69bl-s,13441
8
8
  sweatstack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  sweatstack/schemas.py,sha256=Lkx0rH8LNIHfX8obHSa4EdJF5q4DzDursnhofzSTddQ,134
10
- sweatstack/streamlit.py,sha256=gsgiIDW-INGTvF24ANnX5LJ17ZxnvXx95sjSmtcTlnY,8062
10
+ sweatstack/streamlit.py,sha256=QV_lPykNgcOR1cAO9F0B4hB_sax2IAMZGHOQLwG_0Rw,12368
11
11
  sweatstack/sweatshell.py,sha256=MYLNcWbOdceqKJ3S0Pe8dwHXEeYsGJNjQoYUXpMTftA,333
12
- sweatstack/utils.py,sha256=3K97OSWQy5KZ97QiBbW4Kx1wDGxwyA96ZWpwIbkcQZc,2090
12
+ sweatstack/utils.py,sha256=WMY0THAfktzeDNH3kuwYNtVaoS1OQMaobONtGhpLI2E,2547
13
13
  sweatstack/Sweat Stack examples/Getting started.ipynb,sha256=k2hiSffWecoQ0VxjdpDcgFzBXDQiYEebhnAYlu8cgX8,6335204
14
- sweatstack-0.33.0.dist-info/METADATA,sha256=ULQomRjnvxecWkP2Rg3B6VruKNM26_yfuXZ1jQsTWwI,775
15
- sweatstack-0.33.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- sweatstack-0.33.0.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
17
- sweatstack-0.33.0.dist-info/RECORD,,
14
+ sweatstack-0.35.0.dist-info/METADATA,sha256=QcXvmdaAWgppZNRBa8dlvlpRMNCi02NvJb2KWPAhEts,775
15
+ sweatstack-0.35.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ sweatstack-0.35.0.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
17
+ sweatstack-0.35.0.dist-info/RECORD,,