sweatstack 0.37.0__py3-none-any.whl → 0.40.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
@@ -444,7 +444,7 @@ class Client(OAuth2Mixin, DelegationMixin):
444
444
  tags: list[str] | None = None,
445
445
  limit: int = 100,
446
446
  as_dataframe: bool = False,
447
- ) -> Generator[ActivitySummary, None, None] | pd.DataFrame:
447
+ ) -> list[ActivitySummary] | pd.DataFrame:
448
448
  """Gets a list of activities based on specified filters.
449
449
 
450
450
  Args:
@@ -456,28 +456,27 @@ class Client(OAuth2Mixin, DelegationMixin):
456
456
  as_dataframe: Whether to return results as a pandas DataFrame. Defaults to False.
457
457
 
458
458
  Returns:
459
- Either a generator yielding ActivitySummary objects or a pandas DataFrame containing
459
+ Either a list of ActivitySummary objects or a pandas DataFrame containing
460
460
  the activities data, depending on the value of as_dataframe.
461
461
 
462
462
  Raises:
463
463
  HTTPStatusError: If the API request fails.
464
464
  """
465
- generator = self._get_activities_generator(
465
+ activities = list(self._get_activities_generator(
466
466
  start=start,
467
467
  end=end,
468
468
  sports=sports,
469
469
  tags=tags,
470
470
  limit=limit,
471
- )
471
+ ))
472
472
  if as_dataframe:
473
- df = pd.DataFrame([activity.model_dump() for activity in generator])
474
- df = df.set_index(df["start"].rename("timestamp"))
473
+ df = pd.DataFrame([activity.model_dump() for activity in activities])
475
474
  df = self._normalize_dataframe_column(df, "summary")
476
475
  df = self._normalize_dataframe_column(df, "laps")
477
476
  df = self._normalize_dataframe_column(df, "traces")
478
477
  return self._postprocess_dataframe(df)
479
478
  else:
480
- return generator
479
+ return activities
481
480
 
482
481
  def get_latest_activity(
483
482
  self,
@@ -502,7 +501,7 @@ class Client(OAuth2Mixin, DelegationMixin):
502
501
  StopIteration: If no activities match the filters.
503
502
  HTTPStatusError: If the API request fails.
504
503
  """
505
- return next(self.get_activities(
504
+ return next(self._get_activities_generator(
506
505
  start=start,
507
506
  end=end,
508
507
  sports=[sport] if sport is not None else None,
@@ -841,7 +840,7 @@ class Client(OAuth2Mixin, DelegationMixin):
841
840
  tags: list[str] | None = None,
842
841
  limit: int = 100,
843
842
  as_dataframe: bool = False,
844
- ) -> Generator[TraceDetails, None, None] | pd.DataFrame:
843
+ ) -> list[TraceDetails] | pd.DataFrame:
845
844
  """Gets a list of traces based on specified filters.
846
845
 
847
846
  Args:
@@ -853,23 +852,23 @@ class Client(OAuth2Mixin, DelegationMixin):
853
852
  as_dataframe: Whether to return results as a pandas DataFrame. Defaults to False.
854
853
 
855
854
  Returns:
856
- Either a generator yielding TraceDetails objects or a pandas DataFrame containing
855
+ Either a list of TraceDetails objects or a pandas DataFrame containing
857
856
  the traces data, depending on the value of as_dataframe.
858
857
 
859
858
  Raises:
860
859
  HTTPStatusError: If the API request fails.
861
860
  """
862
- generator = self._get_traces_generator(
861
+ traces = list(self._get_traces_generator(
863
862
  start=start,
864
863
  end=end,
865
864
  sports=sports,
866
865
  tags=tags,
867
866
  limit=limit,
868
- )
867
+ ))
869
868
  if not as_dataframe:
870
- return generator
869
+ return traces
871
870
 
872
- data = pd.DataFrame([trace.model_dump() for trace in generator])
871
+ data = pd.DataFrame([trace.model_dump() for trace in traces])
873
872
 
874
873
  if "activity" in data.columns:
875
874
  data = self._normalize_dataframe_column(data, "activity")
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: openapi.json
3
- # timestamp: 2025-04-08T11:14:52+00:00
3
+ # timestamp: 2025-04-11T08:42:56+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -332,6 +332,8 @@ class Lap(BaseModel):
332
332
  start: datetime = Field(..., title='Start')
333
333
  end: datetime = Field(..., title='End')
334
334
  duration: timedelta = Field(..., title='Duration')
335
+ start_local: datetime = Field(..., title='Start Local')
336
+ end_local: datetime = Field(..., title='End Local')
335
337
 
336
338
 
337
339
  class ActivityDetails(BaseModel):
@@ -347,6 +349,8 @@ class ActivityDetails(BaseModel):
347
349
  traces: Optional[List[TraceDetails]] = Field(None, title='Traces')
348
350
  distance: Optional[float] = Field(None, title='Distance')
349
351
  duration: timedelta = Field(..., title='Duration')
352
+ start_local: datetime = Field(..., title='Start Local')
353
+ end_local: datetime = Field(..., title='End Local')
350
354
 
351
355
 
352
356
  class ActivitySummary(BaseModel):
@@ -361,6 +365,8 @@ class ActivitySummary(BaseModel):
361
365
  laps: Optional[List[Lap]] = Field(None, title='Laps')
362
366
  traces: Optional[List[TraceDetails]] = Field(None, title='Traces')
363
367
  duration: timedelta = Field(..., title='Duration')
368
+ start_local: datetime = Field(..., title='Start Local')
369
+ end_local: datetime = Field(..., title='End Local')
364
370
 
365
371
 
366
372
  class TraceDetails(BaseModel):
@@ -376,6 +382,7 @@ class TraceDetails(BaseModel):
376
382
  lap: Optional[Lap] = None
377
383
  activity: Optional[ActivitySummary] = None
378
384
  sport: Optional[Sport] = None
385
+ timestamp_local: datetime = Field(..., title='Timestamp Local')
379
386
 
380
387
 
381
388
  ActivityDetails.model_rebuild()
sweatstack/schemas.py CHANGED
@@ -1,5 +1,104 @@
1
1
  from enum import Enum
2
+ from typing import List, Union
2
3
 
3
4
  from .openapi_schemas import (
4
- ActivityDetails, ActivitySummary, Metric, Scope, Sport, TraceDetails, UserInfoResponse, UserSummary
5
- )
5
+ ActivityDetails, ActivitySummary, Metric, Scope, Sport,
6
+ TraceDetails, UserInfoResponse, UserSummary
7
+ )
8
+
9
+
10
+ def parent_sport(sport: Sport) -> Sport:
11
+ """Returns the parent sport of a given sport.
12
+
13
+ For sports with a hierarchical structure (e.g., 'cycling.road'), returns the parent sport
14
+ ('cycling'). If the sport has no parent (is already a root sport), returns the sport itself.
15
+
16
+ Args:
17
+ sport (Sport): The sport enum value to find the parent of.
18
+
19
+ Returns:
20
+ Sport: The parent sport enum value, or the original sport if it has no parent.
21
+ """
22
+ parts = sport.value.split(".")
23
+ if len(parts) == 1:
24
+ return sport
25
+ return sport.__class__(".".join(parts[:-1]))
26
+
27
+
28
+ def root_sport(sport: Sport) -> Sport:
29
+ """Returns the root sport of a given sport.
30
+
31
+ For sports with a hierarchical structure (e.g., 'cycling.road' or 'cycling.road.gravel'),
32
+ returns the root sport ('cycling'). If the sport is already a root sport, returns the sport itself.
33
+
34
+ Args:
35
+ sport (Sport): The sport enum value to find the root of.
36
+
37
+ Returns:
38
+ Sport: The root sport enum value.
39
+ """
40
+ return sport.__class__(sport.value.split(".")[0])
41
+
42
+
43
+ def is_root_sport(sport: Sport) -> bool:
44
+ """Determines if a sport is a root sport.
45
+
46
+ A root sport is one that doesn't have a parent sport in the hierarchy
47
+ (e.g., 'cycling' is a root sport, while 'cycling.road' is not).
48
+
49
+ Args:
50
+ sport (Sport): The sport enum value to check.
51
+
52
+ Returns:
53
+ bool: True if the sport is a root sport, False otherwise.
54
+ """
55
+ return sport == root_sport(sport)
56
+
57
+
58
+ def is_sub_sport_of(sport: Sport, sport_or_sports: Union[Sport, List[Sport]]) -> bool:
59
+ """Determines if a sport is a sub-sport of another sport or list of sports.
60
+
61
+ For example, 'cycling.road' is a sub-sport of 'cycling', but not of 'running'.
62
+
63
+ Args:
64
+ sport (Sport): The sport to check.
65
+ sport_or_sports (Union[Sport, List[Sport]]): A sport or list of sports to check against.
66
+
67
+ Returns:
68
+ bool: True if the sport is a sub-sport of any of the provided sports, False otherwise.
69
+
70
+ Raises:
71
+ ValueError: If sport_or_sports is not a Sport or a list of Sports.
72
+ """
73
+ if isinstance(sport_or_sports, Sport):
74
+ return sport.value.startswith(sport_or_sports.value)
75
+ elif isinstance(sport_or_sports, (list, tuple)):
76
+ return any(is_sub_sport_of(sport, sport) for sport in sport_or_sports)
77
+ else:
78
+ raise ValueError(f"Invalid type for sport_or_sports: {type(sport_or_sports)}")
79
+
80
+
81
+ def display_name(sport: Sport) -> str:
82
+ """Returns a human-readable display name for a sport.
83
+
84
+ This function converts a Sport enum value into a formatted string suitable for display.
85
+
86
+ Args:
87
+ sport (Sport): The sport enum value to format.
88
+
89
+ Returns:
90
+ str: A human-readable display name for the sport.
91
+ """
92
+ parts = sport.value.split(".")
93
+ base_sport = parts[0]
94
+ base_sport = base_sport.replace("_", " ")
95
+ if len(parts) == 1:
96
+ return base_sport
97
+ the_rest = " ".join(parts[1:]).replace("_", " ")
98
+ return f"{base_sport} ({the_rest})"
99
+
100
+
101
+ Sport.root_sport = root_sport
102
+ Sport.parent_sport = parent_sport
103
+ Sport.is_sub_sport_of = is_sub_sport_of
104
+ Sport.display_name = display_name
sweatstack/utils.py CHANGED
@@ -52,29 +52,4 @@ def make_dataframe_streamlit_compatible(df: pd.DataFrame) -> pd.DataFrame:
52
52
  lambda x: x.value if isinstance(x, Enum) else x
53
53
  )
54
54
 
55
- return df_copy if df_copy is not None else df
56
-
57
-
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
- """
71
- parts = sport.value.split(".")
72
- base_sport = parts[0]
73
- base_sport = base_sport.replace("_", " ")
74
-
75
- if len(parts) == 1:
76
- return base_sport
77
-
78
- remainder = " ".join(parts[1:]).replace("_", " ")
79
-
80
- return f"{base_sport} ({remainder})"
55
+ return df_copy if df_copy is not None else df
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.37.0
3
+ Version: 0.40.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=KAvrvf-NChmGh7yx7b-kG02MoGPCsEBRdKU1xBanntM,39150
3
+ sweatstack/client.py,sha256=SPacpBpXtPKy6TatnTXe_9jCSo_JY340U_k9FwfnHV8,39048
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
- sweatstack/openapi_schemas.py,sha256=rB_gdkob49AISTebZXmNjOOz3it28Y42_PEKcBF24c0,13576
7
+ sweatstack/openapi_schemas.py,sha256=sXjwCQ-LmMr9nbP9TBFDzwiIbW_f8aJjH4qdwdYgRHQ,13992
8
8
  sweatstack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- sweatstack/schemas.py,sha256=zRxssFvsXmCTtLr69a7n_EHUUbfAG27w6cTr730ZeUY,159
9
+ sweatstack/schemas.py,sha256=d6KRMJalaABO61CPm3afs-hM6zUcwbGrZ5mc6SVuc54,3370
10
10
  sweatstack/streamlit.py,sha256=dtli4RpBiS8cH4GAQ9lDI_ZRDMLoU7GnTc676nLV_4A,13008
11
11
  sweatstack/sweatshell.py,sha256=MYLNcWbOdceqKJ3S0Pe8dwHXEeYsGJNjQoYUXpMTftA,333
12
- sweatstack/utils.py,sha256=WMY0THAfktzeDNH3kuwYNtVaoS1OQMaobONtGhpLI2E,2547
12
+ sweatstack/utils.py,sha256=AwHRdC1ziOZ5o9RBIB21Uxm-DoClVRAJSVvgsmSmvps,1801
13
13
  sweatstack/Sweat Stack examples/Getting started.ipynb,sha256=k2hiSffWecoQ0VxjdpDcgFzBXDQiYEebhnAYlu8cgX8,6335204
14
- sweatstack-0.37.0.dist-info/METADATA,sha256=MQf29vXxKG9b22yMQvH9apE5uWwljU1RY0NQRVSuUng,779
15
- sweatstack-0.37.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- sweatstack-0.37.0.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
17
- sweatstack-0.37.0.dist-info/RECORD,,
14
+ sweatstack-0.40.0.dist-info/METADATA,sha256=z_ucY1E0rPGcDRvIeT2IeSM-_qUszWEc0JwmL22p5cc,779
15
+ sweatstack-0.40.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ sweatstack-0.40.0.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
17
+ sweatstack-0.40.0.dist-info/RECORD,,