sweatstack 0.44.0__tar.gz → 0.46.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. sweatstack-0.46.0/CHANGELOG.md +34 -0
  2. {sweatstack-0.44.0 → sweatstack-0.46.0}/PKG-INFO +2 -1
  3. {sweatstack-0.44.0 → sweatstack-0.46.0}/pyproject.toml +2 -1
  4. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/client.py +27 -0
  5. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/openapi_schemas.py +309 -6
  6. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/schemas.py +12 -1
  7. {sweatstack-0.44.0 → sweatstack-0.46.0}/uv.lock +744 -738
  8. sweatstack-0.44.0/CHANGELOG.md +0 -18
  9. {sweatstack-0.44.0 → sweatstack-0.46.0}/.gitignore +0 -0
  10. {sweatstack-0.44.0 → sweatstack-0.46.0}/.python-version +0 -0
  11. {sweatstack-0.44.0 → sweatstack-0.46.0}/DEVELOPMENT.md +0 -0
  12. {sweatstack-0.44.0 → sweatstack-0.46.0}/Makefile +0 -0
  13. {sweatstack-0.44.0 → sweatstack-0.46.0}/README.md +0 -0
  14. {sweatstack-0.44.0 → sweatstack-0.46.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  15. {sweatstack-0.44.0 → sweatstack-0.46.0}/playground/README.md +0 -0
  16. {sweatstack-0.44.0 → sweatstack-0.46.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  17. {sweatstack-0.44.0 → sweatstack-0.46.0}/playground/Untitled.ipynb +0 -0
  18. {sweatstack-0.44.0 → sweatstack-0.46.0}/playground/hello.py +0 -0
  19. {sweatstack-0.44.0 → sweatstack-0.46.0}/playground/pyproject.toml +0 -0
  20. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  21. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/__init__.py +0 -0
  22. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/cli.py +0 -0
  23. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/constants.py +0 -0
  24. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/ipython_init.py +0 -0
  25. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  26. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/py.typed +0 -0
  27. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/streamlit.py +0 -0
  28. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/sweatshell.py +0 -0
  29. {sweatstack-0.44.0 → sweatstack-0.46.0}/src/sweatstack/utils.py +0 -0
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+
9
+ ## [0.46.0] - 2025-08-01
10
+
11
+ ### Added
12
+
13
+ - Added a new `registered_at` field to the `UserInfoResponse` model that is returned by `ss.get_userinfo()`. This field is the timestamp of the user's registration with SweatStack.
14
+
15
+
16
+
17
+ ## [0.45.0] - 2025-06-24
18
+
19
+ ### Added
20
+
21
+ - Added a new `ss.whoami()` method that returns the authenticated user's summary information. This method is recommended over `ss.get_userinfo()` which only exists for OpenID compatibility and requires the `profile` scope.
22
+ - Added a new `ss.Metric.display_name()` method that returns a human-readable display name for a metric. For example, `ss.Metric.heart_rate.display_name()` returns "heart rate".
23
+
24
+ ## [0.44.0] - 2025-06-18
25
+
26
+ ### Added
27
+
28
+ - Added support for persistent storage of API keys and refresh tokens.
29
+ - Added a new `ss.authenticate()` method that handles authentication comprehensively, including calling `ss.login()` when needed. This method is now the recommended way to authenticate the client.
30
+
31
+
32
+ ## Changed
33
+
34
+ - The `sweatlab` and `sweatshell` commands now use the new `ss.authenticate()` method.
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.44.0
3
+ Version: 0.46.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
7
+ Requires-Dist: email-validator>=2.2.0
7
8
  Requires-Dist: httpx>=0.28.1
8
9
  Requires-Dist: pandas>=2.2.3
9
10
  Requires-Dist: platformdirs>=4.0.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.44.0"
3
+ version = "0.46.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,6 +8,7 @@ authors = [
8
8
  ]
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
11
+ "email-validator>=2.2.0",
11
12
  "httpx>=0.28.1",
12
13
  "pandas>=2.2.3",
13
14
  "platformdirs>=4.0.0",
@@ -1204,6 +1204,32 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
1204
1204
  self._raise_for_status(response)
1205
1205
  return UserInfoResponse.model_validate(response.json())
1206
1206
 
1207
+ def whoami(self) -> UserSummary:
1208
+ """Gets the authenticated user's summary information.
1209
+
1210
+ This method retrieves basic information about the currently authenticated user
1211
+ by extracting the user ID from the JWT token and fetching the user details.
1212
+
1213
+ Returns:
1214
+ UserSummary: A UserSummary object containing the authenticated user's information.
1215
+
1216
+ Raises:
1217
+ ValueError: If no authentication token is available.
1218
+ HTTPStatusError: If the API request fails or user is not found.
1219
+ """
1220
+ if not self.api_key:
1221
+ raise ValueError("Not authenticated. Please call authenticate() or login() first.")
1222
+
1223
+ try:
1224
+ jwt_body = decode_jwt_body(self.api_key)
1225
+ user_id = jwt_body.get("sub")
1226
+ if not user_id:
1227
+ raise ValueError("Unable to extract user ID from token")
1228
+ except Exception as e:
1229
+ raise ValueError(f"Invalid authentication token: {e}")
1230
+
1231
+ return self._get_user_by_id(user_id)
1232
+
1207
1233
 
1208
1234
  _default_client = Client()
1209
1235
 
@@ -1248,6 +1274,7 @@ _generate_singleton_methods(
1248
1274
  "get_user",
1249
1275
  "get_users",
1250
1276
  "get_userinfo",
1277
+ "whoami",
1251
1278
 
1252
1279
  "get_activities",
1253
1280
 
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: openapi.json
3
- # timestamp: 2025-04-15T12:24:50+00:00
3
+ # timestamp: 2025-08-01T16:59:37+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -8,19 +8,44 @@ from datetime import datetime, timedelta
8
8
  from enum import Enum
9
9
  from typing import List, Literal, Optional, Union
10
10
 
11
- from pydantic import AnyUrl, BaseModel, Field, SecretStr, confloat, conint
11
+ from pydantic import (
12
+ AnyUrl,
13
+ BaseModel,
14
+ EmailStr,
15
+ Field,
16
+ RootModel,
17
+ SecretStr,
18
+ confloat,
19
+ conint,
20
+ )
21
+
22
+
23
+ class Activity(BaseModel):
24
+ id: str = Field(..., title='Id')
25
+ start_date_local: datetime = Field(..., title='Start Date Local')
12
26
 
13
27
 
14
28
  class ActivityUpdate(BaseModel):
15
29
  tags: Optional[List[str]] = Field(None, title='Tags')
16
30
 
17
31
 
32
+ class Prompt(Enum):
33
+ none = 'none'
34
+ login = 'login'
35
+ consent = 'consent'
36
+ select_account = 'select_account'
37
+
38
+
39
+ class BodyAddEmailPartialsAddEmailPost(BaseModel):
40
+ email: EmailStr = Field(..., title='Email')
41
+
42
+
18
43
  class BodyCreateApplicationApplicationsPost(BaseModel):
19
44
  name: str = Field(..., title='Name')
20
45
  description: str = Field(..., title='Description')
21
46
  url: AnyUrl = Field(..., title='Url')
22
47
  image: AnyUrl = Field(..., title='Image')
23
- redirect_uris: List[AnyUrl] = Field(..., title='Redirect Uris')
48
+ redirect_uris: List[str] = Field(..., title='Redirect Uris')
24
49
  privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
25
50
  published: Optional[bool] = Field(False, title='Published')
26
51
 
@@ -133,6 +158,7 @@ class Tz(Enum):
133
158
  America_Coral_Harbour = 'America/Coral_Harbour'
134
159
  America_Cordoba = 'America/Cordoba'
135
160
  America_Costa_Rica = 'America/Costa_Rica'
161
+ America_Coyhaique = 'America/Coyhaique'
136
162
  America_Creston = 'America/Creston'
137
163
  America_Cuiaba = 'America/Cuiaba'
138
164
  America_Curacao = 'America/Curacao'
@@ -635,13 +661,13 @@ class Tz(Enum):
635
661
 
636
662
 
637
663
  class BodyLoginPostLoginPost(BaseModel):
638
- email: str = Field(..., title='Email')
664
+ email: EmailStr = Field(..., title='Email')
639
665
  password: SecretStr = Field(..., title='Password')
640
666
  tz: Tz = Field(..., title='Tz')
641
667
 
642
668
 
643
669
  class BodyRegisterPostRegisterPost(BaseModel):
644
- email: str = Field(..., title='Email')
670
+ email: EmailStr = Field(..., title='Email')
645
671
  password: SecretStr = Field(..., title='Password')
646
672
  tz: Tz = Field(..., title='Tz')
647
673
 
@@ -654,12 +680,17 @@ class BodySaveOrUpdateIntegrationProviderTenantsTenantIdIntegrationProvidersInte
654
680
  redirect_url: str = Field(..., title='Redirect Url')
655
681
 
656
682
 
683
+ class BodySendVerificationEmailPartialsSendVerificationEmailPost(BaseModel):
684
+ email: EmailStr = Field(..., title='Email')
685
+ tz: Tz = Field(..., title='Tz')
686
+
687
+
657
688
  class BodyUpdateApplicationApplicationsApplicationIdPut(BaseModel):
658
689
  name: str = Field(..., title='Name')
659
690
  description: str = Field(..., title='Description')
660
691
  url: AnyUrl = Field(..., title='Url')
661
692
  image: AnyUrl = Field(..., title='Image')
662
- redirect_uris: List[AnyUrl] = Field(..., title='Redirect Uris')
693
+ redirect_uris: List[str] = Field(..., title='Redirect Uris')
663
694
  privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
664
695
  published: Optional[bool] = Field(False, title='Published')
665
696
 
@@ -669,6 +700,10 @@ class BodyUpdateUserUsersUserIdPut(BaseModel):
669
700
  last_name: str = Field(..., title='Last Name')
670
701
 
671
702
 
703
+ class BodyUploadActivityFileApiV1ActivitiesUploadPost(BaseModel):
704
+ files: List[bytes] = Field(..., max_length=10, title='Files')
705
+
706
+
672
707
  class BodyUploadActivityFileDataUploadPost(BaseModel):
673
708
  files: List[bytes] = Field(..., title='Files')
674
709
 
@@ -690,6 +725,13 @@ class ElevationSummary(BaseModel):
690
725
  max: Optional[int] = Field(None, title='Max')
691
726
 
692
727
 
728
+ class GarminBackfillBody(BaseModel):
729
+ start: datetime = Field(..., title='Start')
730
+ end: datetime = Field(..., title='End')
731
+ user_id__in: Optional[List[str]] = Field(None, title='User Id In')
732
+ user_id__not_in: Optional[List[str]] = Field(None, title='User Id Not In')
733
+
734
+
693
735
  class GarminDeregistration(BaseModel):
694
736
  userId: str = Field(..., title='Userid')
695
737
  userAccessToken: str = Field(..., title='Useraccesstoken')
@@ -718,8 +760,33 @@ class HeartRateSummary(BaseModel):
718
760
  end: Optional[float] = Field(None, title='End')
719
761
 
720
762
 
763
+ class Instruction(BaseModel):
764
+ type: Literal['instruction'] = Field('instruction', title='Type')
765
+ text: str = Field(..., title='Text')
766
+
767
+
721
768
  class IntegrationName(Enum):
722
769
  garmin_connect = 'garmin_connect'
770
+ intervals_icu = 'intervals_icu'
771
+
772
+
773
+ class IntensityQuantity(Enum):
774
+ speed = 'speed'
775
+ power = 'power'
776
+
777
+
778
+ class IntervalsIcuWebhookEventType(Enum):
779
+ APP_SCOPE_CHANGED = 'APP_SCOPE_CHANGED'
780
+ CALENDAR_UPDATED = 'CALENDAR_UPDATED'
781
+ CALENDAR_EVENT_UPDATED = 'CALENDAR_EVENT_UPDATED'
782
+ CALENDAR_EVENT_DELETED = 'CALENDAR_EVENT_DELETED'
783
+ ACTIVITY_UPLOADED = 'ACTIVITY_UPLOADED'
784
+ ACTIVITY_ANALYZED = 'ACTIVITY_ANALYZED'
785
+ ACTIVITY_UPDATED = 'ACTIVITY_UPDATED'
786
+ ACTIVITY_DELETED = 'ACTIVITY_DELETED'
787
+ ACTIVITY_ACHIEVEMENTS = 'ACTIVITY_ACHIEVEMENTS'
788
+ WELLNESS_UPDATED = 'WELLNESS_UPDATED'
789
+ FITNESS_UPDATED = 'FITNESS_UPDATED'
723
790
 
724
791
 
725
792
  class Metric(Enum):
@@ -748,6 +815,16 @@ class PowerSummary(BaseModel):
748
815
  max: Optional[float] = Field(None, title='Max')
749
816
 
750
817
 
818
+ class Reference(Enum):
819
+ absolute = 'absolute'
820
+ tte = 'tte'
821
+ parameter = 'parameter'
822
+
823
+
824
+ class RepeatQuantity(Enum):
825
+ number = 'number'
826
+
827
+
751
828
  class Scope(Enum):
752
829
  data_read = 'data:read'
753
830
  data_write = 'data:write'
@@ -819,6 +896,22 @@ class TemperatureSummary(BaseModel):
819
896
  max: Optional[float] = Field(None, title='Max')
820
897
 
821
898
 
899
+ class TemplateDay(BaseModel):
900
+ sports: List[Sport] = Field(..., title='Sports')
901
+ avg_training_duration: timedelta = Field(..., title='Avg Training Duration')
902
+ typical_training_duration: timedelta = Field(..., title='Typical Training Duration')
903
+
904
+
905
+ class TemplateWeekResponse(BaseModel):
906
+ monday: TemplateDay
907
+ tuesday: TemplateDay
908
+ wednesday: TemplateDay
909
+ thursday: TemplateDay
910
+ friday: TemplateDay
911
+ saturday: TemplateDay
912
+ sunday: TemplateDay
913
+
914
+
822
915
  class TokenRequest(BaseModel):
823
916
  grant_type: GrantType
824
917
  client_id: Optional[str] = Field(None, title='Client Id')
@@ -850,11 +943,18 @@ class TraceCreateOrUpdate(BaseModel):
850
943
  sport: Optional[Sport] = None
851
944
 
852
945
 
946
+ class UserFlow(Enum):
947
+ session = 'session'
948
+ signup = 'signup'
949
+ login = 'login'
950
+
951
+
853
952
  class UserInfoResponse(BaseModel):
854
953
  sub: str = Field(..., title='Sub')
855
954
  given_name: Optional[str] = Field(None, title='Given Name')
856
955
  family_name: Optional[str] = Field(None, title='Family Name')
857
956
  email: Optional[str] = Field(None, title='Email')
957
+ registered_at: datetime = Field(..., title='Registered At')
858
958
  name: str = Field(..., title='Name')
859
959
 
860
960
 
@@ -872,6 +972,17 @@ class ValidationError(BaseModel):
872
972
  type: str = Field(..., title='Error Type')
873
973
 
874
974
 
975
+ class Value(BaseModel):
976
+ reference: Reference
977
+ value: Union[int, float] = Field(..., title='Value')
978
+ parameter: Optional[str] = Field(None, title='Parameter')
979
+
980
+
981
+ class VolumeQuantity(Enum):
982
+ duration = 'duration'
983
+ distance = 'distance'
984
+
985
+
875
986
  class ActivitySummarySummary(BaseModel):
876
987
  power: Optional[PowerSummary] = None
877
988
  speed: Optional[SpeedSummary] = None
@@ -883,6 +994,18 @@ class ActivitySummarySummary(BaseModel):
883
994
  smo2: Optional[Smo2Summary] = None
884
995
 
885
996
 
997
+ class AuthorizeQueryParams(BaseModel):
998
+ client_id: str = Field(..., title='Client Id')
999
+ redirect_uri: Optional[str] = Field(None, title='Redirect Uri')
1000
+ scope: List[Scope] = Field(..., min_length=1, title='Scope')
1001
+ state: Optional[str] = Field(None, title='State')
1002
+ nonce: Optional[str] = Field(None, title='Nonce')
1003
+ code_challenge: Optional[str] = Field(None, title='Code Challenge')
1004
+ code_challenge_method: Optional[str] = Field(None, title='Code Challenge Method')
1005
+ prompt: Optional[Prompt] = Field('consent', title='Prompt')
1006
+ user_flow: Optional[UserFlow] = 'session'
1007
+
1008
+
886
1009
  class BodyAuthorizeOauthAuthorizePost(BaseModel):
887
1010
  client_id: str = Field(..., title='Client Id')
888
1011
  redirect_uri: Optional[str] = Field(None, title='Redirect Uri')
@@ -893,10 +1016,32 @@ class BodyAuthorizeOauthAuthorizePost(BaseModel):
893
1016
  code_challenge_method: Optional[str] = Field(None, title='Code Challenge Method')
894
1017
 
895
1018
 
1019
+ class BodyExpressAddEmailPostExpressAddEmailPost(BaseModel):
1020
+ email: EmailStr = Field(..., title='Email')
1021
+ state: Optional[str] = Field(None, title='State')
1022
+ user_flow: Optional[UserFlow] = 'session'
1023
+
1024
+
896
1025
  class BodyGenerateApiKeyPartialsGenerateApiKeyPost(BaseModel):
897
1026
  scopes: List[Scope] = Field(..., title='Scopes')
898
1027
 
899
1028
 
1029
+ class ConstantValueInput(BaseModel):
1030
+ type: Literal['constant'] = Field('constant', title='Type')
1031
+ quantity: Union[VolumeQuantity, IntensityQuantity, RepeatQuantity] = Field(
1032
+ ..., title='Quantity'
1033
+ )
1034
+ value: Value
1035
+
1036
+
1037
+ class ConstantValueOutput(BaseModel):
1038
+ type: Literal['constant'] = Field('constant', title='Type')
1039
+ quantity: Union[VolumeQuantity, IntensityQuantity, RepeatQuantity] = Field(
1040
+ ..., title='Quantity'
1041
+ )
1042
+ value: Value
1043
+
1044
+
900
1045
  class DelegatedTokenRequest(BaseModel):
901
1046
  sub: str = Field(..., title='Sub')
902
1047
  scopes: Optional[List[Scope]] = Field(None, title='Scopes')
@@ -922,6 +1067,13 @@ class HTTPValidationError(BaseModel):
922
1067
  detail: Optional[List[ValidationError]] = Field(None, title='Detail')
923
1068
 
924
1069
 
1070
+ class IntervalsIcuWebhookEvent(BaseModel):
1071
+ athlete_id: str = Field(..., title='Athlete Id')
1072
+ type: IntervalsIcuWebhookEventType
1073
+ timestamp: datetime = Field(..., title='Timestamp')
1074
+ activity: Activity
1075
+
1076
+
925
1077
  class Lap(BaseModel):
926
1078
  power: Optional[PowerSummary] = None
927
1079
  speed: Optional[SpeedSummary] = None
@@ -938,6 +1090,155 @@ class Lap(BaseModel):
938
1090
  end_local: datetime = Field(..., title='End Local')
939
1091
 
940
1092
 
1093
+ class RampValueInput(BaseModel):
1094
+ type: Literal['ramp'] = Field('ramp', title='Type')
1095
+ quantity: Union[VolumeQuantity, IntensityQuantity, RepeatQuantity] = Field(
1096
+ ..., title='Quantity'
1097
+ )
1098
+ start: Value
1099
+ end: Value
1100
+
1101
+
1102
+ class RampValueOutput(BaseModel):
1103
+ type: Literal['ramp'] = Field('ramp', title='Type')
1104
+ quantity: Union[VolumeQuantity, IntensityQuantity, RepeatQuantity] = Field(
1105
+ ..., title='Quantity'
1106
+ )
1107
+ start: Value
1108
+ end: Value
1109
+
1110
+
1111
+ class RangeValueInput(BaseModel):
1112
+ type: Literal['range'] = Field('range', title='Type')
1113
+ quantity: Union[VolumeQuantity, IntensityQuantity, RepeatQuantity] = Field(
1114
+ ..., title='Quantity'
1115
+ )
1116
+ min: Optional[Value] = None
1117
+ max: Optional[Value] = None
1118
+
1119
+
1120
+ class RangeValueOutput(BaseModel):
1121
+ type: Literal['range'] = Field('range', title='Type')
1122
+ quantity: Union[VolumeQuantity, IntensityQuantity, RepeatQuantity] = Field(
1123
+ ..., title='Quantity'
1124
+ )
1125
+ min: Optional[Value] = None
1126
+ max: Optional[Value] = None
1127
+
1128
+
1129
+ class IntervalInput(BaseModel):
1130
+ type: Literal['interval'] = Field('interval', title='Type')
1131
+ volume: Union[ConstantValueInput, RangeValueInput, RampValueInput] = Field(
1132
+ ..., discriminator='type', title='Volume'
1133
+ )
1134
+ intensity: Union[ConstantValueInput, RangeValueInput, RampValueInput] = Field(
1135
+ ..., discriminator='type', title='Intensity'
1136
+ )
1137
+
1138
+
1139
+ class IntervalOutput(BaseModel):
1140
+ type: Literal['interval'] = Field('interval', title='Type')
1141
+ volume: Union[ConstantValueOutput, RangeValueOutput, RampValueOutput] = Field(
1142
+ ..., discriminator='type', title='Volume'
1143
+ )
1144
+ intensity: Union[ConstantValueOutput, RangeValueOutput, RampValueOutput] = Field(
1145
+ ..., discriminator='type', title='Intensity'
1146
+ )
1147
+
1148
+
1149
+ class IntervalsIcuWebhookBody(BaseModel):
1150
+ secret: str = Field(..., title='Secret')
1151
+ events: List[IntervalsIcuWebhookEvent] = Field(..., title='Events')
1152
+
1153
+
1154
+ class RepeatInput(BaseModel):
1155
+ type: Literal['repeat'] = Field(..., title='Type')
1156
+ count: Union[ConstantValueInput, RangeValueInput] = Field(
1157
+ ..., discriminator='type', title='Count'
1158
+ )
1159
+ content: List[Union[IntervalInput, RepeatInput, Instruction]] = Field(
1160
+ ..., title='Content'
1161
+ )
1162
+
1163
+
1164
+ class RepeatOutput(BaseModel):
1165
+ type: Literal['repeat'] = Field(..., title='Type')
1166
+ count: Union[ConstantValueOutput, RangeValueOutput] = Field(
1167
+ ..., discriminator='type', title='Count'
1168
+ )
1169
+ content: List[Union[IntervalOutput, RepeatOutput, Instruction]] = Field(
1170
+ ..., title='Content'
1171
+ )
1172
+
1173
+
1174
+ class SectionInput(BaseModel):
1175
+ type: Literal['section'] = Field('section', title='Type')
1176
+ name: Optional[str] = Field(..., title='Name')
1177
+ content: List[Union[IntervalInput, RepeatInput, Instruction]] = Field(
1178
+ ..., title='Content'
1179
+ )
1180
+
1181
+
1182
+ class SectionOutput(BaseModel):
1183
+ type: Literal['section'] = Field('section', title='Type')
1184
+ name: Optional[str] = Field(..., title='Name')
1185
+ content: List[Union[IntervalOutput, RepeatOutput, Instruction]] = Field(
1186
+ ..., title='Content'
1187
+ )
1188
+
1189
+
1190
+ class Content(RootModel[Union[IntervalInput, RepeatInput, SectionInput, Instruction]]):
1191
+ root: Union[IntervalInput, RepeatInput, SectionInput, Instruction] = Field(
1192
+ ..., discriminator='type'
1193
+ )
1194
+
1195
+
1196
+ class WorkoutInput(BaseModel):
1197
+ version: Optional[str] = Field('0.1.0', title='Version')
1198
+ title: Optional[str] = Field(None, title='Title')
1199
+ description: Optional[str] = Field(None, title='Description')
1200
+ content: List[Content] = Field(..., title='Content')
1201
+
1202
+
1203
+ class Content1(
1204
+ RootModel[Union[IntervalOutput, RepeatOutput, SectionOutput, Instruction]]
1205
+ ):
1206
+ root: Union[IntervalOutput, RepeatOutput, SectionOutput, Instruction] = Field(
1207
+ ..., discriminator='type'
1208
+ )
1209
+
1210
+
1211
+ class WorkoutOutput(BaseModel):
1212
+ version: Optional[str] = Field('0.1.0', title='Version')
1213
+ title: Optional[str] = Field(None, title='Title')
1214
+ description: Optional[str] = Field(None, title='Description')
1215
+ content: List[Content1] = Field(..., title='Content')
1216
+
1217
+
1218
+ class LibraryWorkoutCreate(BaseModel):
1219
+ swf: WorkoutInput
1220
+ sport: Sport
1221
+
1222
+
1223
+ class LibraryWorkoutResponse(BaseModel):
1224
+ id: str = Field(..., title='Id')
1225
+ swf: WorkoutOutput
1226
+ sport: Sport
1227
+
1228
+
1229
+ class ScheduledWorkoutCreate(BaseModel):
1230
+ swf: WorkoutInput
1231
+ sport: Sport
1232
+ start: datetime = Field(..., title='Start')
1233
+
1234
+
1235
+ class ScheduledWorkoutResponse(BaseModel):
1236
+ id: str = Field(..., title='Id')
1237
+ swf: WorkoutOutput
1238
+ sport: Sport
1239
+ start: Optional[datetime] = Field(..., title='Start')
1240
+
1241
+
941
1242
  class ActivityDetails(BaseModel):
942
1243
  tags: Optional[List[str]] = Field(None, title='Tags')
943
1244
  id: str = Field(..., title='Id')
@@ -987,5 +1288,7 @@ class TraceDetails(BaseModel):
987
1288
  timestamp_local: datetime = Field(..., title='Timestamp Local')
988
1289
 
989
1290
 
1291
+ RepeatInput.model_rebuild()
1292
+ RepeatOutput.model_rebuild()
990
1293
  ActivityDetails.model_rebuild()
991
1294
  ActivitySummary.model_rebuild()
@@ -101,4 +101,15 @@ def display_name(sport: Sport) -> str:
101
101
  Sport.root_sport = root_sport
102
102
  Sport.parent_sport = parent_sport
103
103
  Sport.is_sub_sport_of = is_sub_sport_of
104
- Sport.display_name = display_name
104
+ Sport.display_name = display_name
105
+
106
+
107
+ def metric_display_name(metric: Metric) -> str:
108
+ """Returns a human-readable display name for a metric.
109
+
110
+ This function converts a Metric enum value into a formatted string suitable for display.
111
+ """
112
+ return metric.value.replace("_", " ")
113
+
114
+
115
+ Metric.display_name = metric_display_name