otf-api 0.2.1__py3-none-any.whl → 0.3.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.
- otf_api/__init__.py +12 -67
- otf_api/api.py +794 -36
- otf_api/cli/__init__.py +4 -0
- otf_api/cli/_utilities.py +60 -0
- otf_api/cli/app.py +177 -0
- otf_api/cli/bookings.py +231 -0
- otf_api/cli/prompts.py +162 -0
- otf_api/models/__init__.py +4 -8
- otf_api/models/auth.py +18 -12
- otf_api/models/base.py +205 -2
- otf_api/models/responses/__init__.py +6 -14
- otf_api/models/responses/body_composition_list.py +304 -0
- otf_api/models/responses/book_class.py +405 -0
- otf_api/models/responses/bookings.py +211 -37
- otf_api/models/responses/cancel_booking.py +93 -0
- otf_api/models/responses/challenge_tracker_content.py +6 -6
- otf_api/models/responses/challenge_tracker_detail.py +6 -6
- otf_api/models/responses/classes.py +205 -7
- otf_api/models/responses/enums.py +0 -35
- otf_api/models/responses/favorite_studios.py +5 -5
- otf_api/models/responses/latest_agreement.py +2 -2
- otf_api/models/responses/lifetime_stats.py +92 -0
- otf_api/models/responses/member_detail.py +17 -12
- otf_api/models/responses/member_membership.py +2 -2
- otf_api/models/responses/member_purchases.py +9 -9
- otf_api/models/responses/out_of_studio_workout_history.py +4 -4
- otf_api/models/responses/performance_summary_detail.py +1 -1
- otf_api/models/responses/performance_summary_list.py +13 -13
- otf_api/models/responses/studio_detail.py +10 -10
- otf_api/models/responses/studio_services.py +8 -8
- otf_api/models/responses/telemetry.py +6 -6
- otf_api/models/responses/telemetry_hr_history.py +6 -6
- otf_api/models/responses/telemetry_max_hr.py +3 -3
- otf_api/models/responses/total_classes.py +2 -2
- otf_api/models/responses/workouts.py +4 -4
- otf_api-0.3.0.dist-info/METADATA +55 -0
- otf_api-0.3.0.dist-info/RECORD +42 -0
- otf_api-0.3.0.dist-info/entry_points.txt +3 -0
- otf_api/__version__.py +0 -1
- otf_api/classes_api.py +0 -44
- otf_api/member_api.py +0 -380
- otf_api/performance_api.py +0 -54
- otf_api/studios_api.py +0 -96
- otf_api/telemetry_api.py +0 -95
- otf_api-0.2.1.dist-info/METADATA +0 -284
- otf_api-0.2.1.dist-info/RECORD +0 -38
- {otf_api-0.2.1.dist-info → otf_api-0.3.0.dist-info}/AUTHORS.md +0 -0
- {otf_api-0.2.1.dist-info → otf_api-0.3.0.dist-info}/LICENSE +0 -0
- {otf_api-0.2.1.dist-info → otf_api-0.3.0.dist-info}/WHEEL +0 -0
otf_api/models/auth.py
CHANGED
@@ -5,13 +5,13 @@ from typing import ClassVar
|
|
5
5
|
from pycognito import Cognito, TokenVerificationException
|
6
6
|
from pydantic import Field
|
7
7
|
|
8
|
-
from otf_api.models.base import
|
8
|
+
from otf_api.models.base import OtfItemBase
|
9
9
|
|
10
10
|
CLIENT_ID = "65knvqta6p37efc2l3eh26pl5o" # from otlive
|
11
11
|
USER_POOL_ID = "us-east-1_dYDxUeyL1"
|
12
12
|
|
13
13
|
|
14
|
-
class IdClaimsData(
|
14
|
+
class IdClaimsData(OtfItemBase):
|
15
15
|
sub: str
|
16
16
|
email_verified: bool
|
17
17
|
iss: str
|
@@ -28,6 +28,7 @@ class IdClaimsData(OtfBaseModel):
|
|
28
28
|
iat: int
|
29
29
|
family_name: str
|
30
30
|
email: str
|
31
|
+
koji_person_id: str = Field(alias="custom:koji_person_id")
|
31
32
|
|
32
33
|
@property
|
33
34
|
def member_uuid(self) -> str:
|
@@ -38,7 +39,7 @@ class IdClaimsData(OtfBaseModel):
|
|
38
39
|
return f"{self.given_name} {self.family_name}"
|
39
40
|
|
40
41
|
|
41
|
-
class AccessClaimsData(
|
42
|
+
class AccessClaimsData(OtfItemBase):
|
42
43
|
sub: str
|
43
44
|
device_key: str
|
44
45
|
iss: str
|
@@ -91,18 +92,25 @@ class User:
|
|
91
92
|
self.token_path.write_text(json.dumps(data))
|
92
93
|
|
93
94
|
@classmethod
|
94
|
-
def
|
95
|
+
def cache_file_exists(cls) -> bool:
|
96
|
+
return cls.token_path.exists()
|
97
|
+
|
98
|
+
@classmethod
|
99
|
+
def username_from_disk(cls) -> str:
|
100
|
+
val: str = json.loads(cls.token_path.read_text())["username"]
|
101
|
+
return val
|
102
|
+
|
103
|
+
@classmethod
|
104
|
+
def load_from_disk(cls, username: str, password: str) -> "User":
|
95
105
|
"""Load a User instance from disk. If the token is invalid, reauthenticate with the provided credentials.
|
96
106
|
|
97
107
|
Args:
|
98
|
-
username (str
|
99
|
-
password (str
|
108
|
+
username (str): The username to reauthenticate with.
|
109
|
+
password (str): The password to reauthenticate with.
|
100
110
|
|
101
111
|
Returns:
|
102
112
|
User: The loaded user.
|
103
113
|
|
104
|
-
Raises:
|
105
|
-
ValueError: If the token is invalid and no username and password are provided.
|
106
114
|
"""
|
107
115
|
attr_dict = json.loads(cls.token_path.read_text())
|
108
116
|
|
@@ -111,10 +119,8 @@ class User:
|
|
111
119
|
cognito_user.verify_tokens()
|
112
120
|
return cls(cognito=cognito_user)
|
113
121
|
except TokenVerificationException:
|
114
|
-
|
115
|
-
|
116
|
-
return user
|
117
|
-
raise
|
122
|
+
user = cls.login(username, password)
|
123
|
+
return user
|
118
124
|
|
119
125
|
@classmethod
|
120
126
|
def login(cls, username: str, password: str) -> "User":
|
otf_api/models/base.py
CHANGED
@@ -1,7 +1,210 @@
|
|
1
|
-
|
1
|
+
import inspect
|
2
|
+
import typing
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Any, ClassVar, TypeVar
|
2
5
|
|
6
|
+
from box import Box
|
7
|
+
from inflection import humanize
|
3
8
|
from pydantic import BaseModel, ConfigDict
|
9
|
+
from rich.style import Style
|
10
|
+
from rich.styled import Styled
|
11
|
+
from rich.table import Table
|
4
12
|
|
13
|
+
if typing.TYPE_CHECKING:
|
14
|
+
from pydantic.main import IncEx
|
5
15
|
|
6
|
-
|
16
|
+
T = TypeVar("T", bound="OtfItemBase")
|
17
|
+
|
18
|
+
|
19
|
+
class BetterDumperMixin:
|
20
|
+
"""A better dumper for Pydantic models that includes properties in the dumped data. Must be mixed
|
21
|
+
into a Pydantic model, as it overrides the `model_dump` method.
|
22
|
+
|
23
|
+
Includes support for nested models, and has an option to not include properties when dumping.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def get_properties(self) -> list[str]:
|
27
|
+
"""Get the properties of the model."""
|
28
|
+
cls = type(self)
|
29
|
+
|
30
|
+
properties: list[str] = []
|
31
|
+
methods = inspect.getmembers(self, lambda f: not (inspect.isroutine(f)))
|
32
|
+
for prop_name, _ in methods:
|
33
|
+
if hasattr(cls, prop_name) and isinstance(getattr(cls, prop_name), property):
|
34
|
+
properties.append(prop_name)
|
35
|
+
|
36
|
+
return properties
|
37
|
+
|
38
|
+
@typing.overload
|
39
|
+
def model_dump(
|
40
|
+
self,
|
41
|
+
*,
|
42
|
+
mode: typing.Literal["json", "python"] | str = "python",
|
43
|
+
include: "IncEx" = None,
|
44
|
+
exclude: "IncEx" = None,
|
45
|
+
by_alias: bool = False,
|
46
|
+
exclude_unset: bool = False,
|
47
|
+
exclude_defaults: bool = False,
|
48
|
+
exclude_none: bool = False,
|
49
|
+
round_trip: bool = False,
|
50
|
+
warnings: bool = True,
|
51
|
+
include_properties: bool = True,
|
52
|
+
) -> Box[str, typing.Any]: ...
|
53
|
+
|
54
|
+
@typing.overload
|
55
|
+
def model_dump(
|
56
|
+
self,
|
57
|
+
*,
|
58
|
+
mode: typing.Literal["json", "python"] | str = "python",
|
59
|
+
include: "IncEx" = None,
|
60
|
+
exclude: "IncEx" = None,
|
61
|
+
by_alias: bool = False,
|
62
|
+
exclude_unset: bool = False,
|
63
|
+
exclude_defaults: bool = False,
|
64
|
+
exclude_none: bool = False,
|
65
|
+
round_trip: bool = False,
|
66
|
+
warnings: bool = True,
|
67
|
+
include_properties: bool = False,
|
68
|
+
) -> dict[str, typing.Any]: ...
|
69
|
+
|
70
|
+
def model_dump(
|
71
|
+
self,
|
72
|
+
*,
|
73
|
+
mode: typing.Literal["json", "python"] | str = "python",
|
74
|
+
include: "IncEx" = None,
|
75
|
+
exclude: "IncEx" = None,
|
76
|
+
by_alias: bool = False,
|
77
|
+
exclude_unset: bool = False,
|
78
|
+
exclude_defaults: bool = False,
|
79
|
+
exclude_none: bool = False,
|
80
|
+
round_trip: bool = False,
|
81
|
+
warnings: bool = True,
|
82
|
+
include_properties: bool = True,
|
83
|
+
) -> dict[str, typing.Any] | Box[str, typing.Any]:
|
84
|
+
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
|
85
|
+
|
86
|
+
Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
mode: The mode in which `to_python` should run.
|
90
|
+
If mode is 'json', the dictionary will only contain JSON serializable types.
|
91
|
+
If mode is 'python', the dictionary may contain any Python objects.
|
92
|
+
include: A list of fields to include in the output.
|
93
|
+
exclude: A list of fields to exclude from the output.
|
94
|
+
by_alias: Whether to use the field's alias in the dictionary key if defined.
|
95
|
+
exclude_unset: Whether to exclude fields that are unset or None from the output.
|
96
|
+
exclude_defaults: Whether to exclude fields that are set to their default value from the output.
|
97
|
+
exclude_none: Whether to exclude fields that have a value of `None` from the output.
|
98
|
+
round_trip: Whether to enable serialization and deserialization round-trip support.
|
99
|
+
warnings: Whether to log warnings when invalid fields are encountered.
|
100
|
+
include_properties: Whether to include properties in the dumped data.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
A dictionary representation of the model. Will be a `Box` if `include_properties` is `True`, otherwise a
|
104
|
+
regular dictionary.
|
105
|
+
|
106
|
+
"""
|
107
|
+
dumped_data = typing.cast(BaseModel, super()).model_dump(
|
108
|
+
mode=mode,
|
109
|
+
include=include,
|
110
|
+
exclude=exclude,
|
111
|
+
by_alias=by_alias,
|
112
|
+
exclude_unset=exclude_unset,
|
113
|
+
exclude_defaults=exclude_defaults,
|
114
|
+
exclude_none=exclude_none,
|
115
|
+
round_trip=round_trip,
|
116
|
+
warnings=warnings,
|
117
|
+
)
|
118
|
+
|
119
|
+
if not include_properties:
|
120
|
+
return dumped_data
|
121
|
+
|
122
|
+
properties = self.get_properties()
|
123
|
+
|
124
|
+
# set properties to their values
|
125
|
+
for prop_name in properties:
|
126
|
+
dumped_data[prop_name] = getattr(self, prop_name)
|
127
|
+
|
128
|
+
# if the property is a Pydantic model, dump it as well
|
129
|
+
for k, v in dumped_data.items():
|
130
|
+
if issubclass(type(getattr(self, k)), BaseModel):
|
131
|
+
dumped_data[k] = getattr(self, k).model_dump()
|
132
|
+
elif hasattr(v, "model_dump"):
|
133
|
+
dumped_data[k] = v.model_dump()
|
134
|
+
|
135
|
+
return Box(dumped_data)
|
136
|
+
|
137
|
+
|
138
|
+
class OtfItemBase(BetterDumperMixin, BaseModel):
|
7
139
|
model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
|
140
|
+
|
141
|
+
def convert_row_value_types(self, row: list[Any]) -> list[str]:
|
142
|
+
for i, val in enumerate(row):
|
143
|
+
if isinstance(val, bool):
|
144
|
+
row[i] = str(val)
|
145
|
+
continue
|
146
|
+
|
147
|
+
if isinstance(val, Enum):
|
148
|
+
row[i] = val.name
|
149
|
+
continue
|
150
|
+
|
151
|
+
if val is None:
|
152
|
+
row[i] = ""
|
153
|
+
continue
|
154
|
+
|
155
|
+
row[i] = str(val)
|
156
|
+
|
157
|
+
return row
|
158
|
+
|
159
|
+
def get_style(self, is_selected: bool = False) -> Style:
|
160
|
+
return Style(color="blue", bold=True) if is_selected else Style(color="white")
|
161
|
+
|
162
|
+
def to_row(self, attributes: list[str], is_selected: bool = False) -> list[Styled]:
|
163
|
+
style = self.get_style(is_selected)
|
164
|
+
|
165
|
+
boxed_self = Box(self.model_dump(), box_dots=True)
|
166
|
+
row = [boxed_self.get(attr, "") for attr in attributes]
|
167
|
+
row = self.convert_row_value_types(row)
|
168
|
+
styled = [Styled(cell, style=style) for cell in row]
|
169
|
+
|
170
|
+
prefix = "> " if is_selected else " "
|
171
|
+
styled.insert(0, Styled(prefix, style=style))
|
172
|
+
|
173
|
+
return styled
|
174
|
+
|
175
|
+
@property
|
176
|
+
def sidebar_data(self) -> Table | None:
|
177
|
+
return None
|
178
|
+
|
179
|
+
@classmethod
|
180
|
+
def attr_to_column_header(cls, attr: str) -> str:
|
181
|
+
attr_map = {k: humanize(k) for k in cls.model_fields}
|
182
|
+
|
183
|
+
return attr_map.get(attr, attr)
|
184
|
+
|
185
|
+
|
186
|
+
class OtfListBase(BaseModel):
|
187
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
|
188
|
+
collection_field: ClassVar[str] = "data"
|
189
|
+
|
190
|
+
@property
|
191
|
+
def collection(self) -> list[OtfItemBase]:
|
192
|
+
return getattr(self, self.collection_field)
|
193
|
+
|
194
|
+
def to_table(self, columns: list[str]) -> Table:
|
195
|
+
table = Table(expand=True, show_header=True, show_footer=False)
|
196
|
+
|
197
|
+
table.add_column()
|
198
|
+
for column in columns:
|
199
|
+
table.add_column(OtfItemBase.attr_to_column_header(column))
|
200
|
+
|
201
|
+
for item in self.collection:
|
202
|
+
table.add_row(*item.to_row(columns))
|
203
|
+
|
204
|
+
return table
|
205
|
+
|
206
|
+
def to_json(self, **kwargs) -> str:
|
207
|
+
kwargs.setdefault("indent", 4)
|
208
|
+
kwargs.setdefault("exclude_none", True)
|
209
|
+
|
210
|
+
return self.model_dump_json(**kwargs)
|
@@ -1,17 +1,10 @@
|
|
1
|
-
from .
|
1
|
+
from .book_class import BookClass
|
2
|
+
from .bookings import BookingList, BookingStatus
|
3
|
+
from .cancel_booking import CancelBooking
|
2
4
|
from .challenge_tracker_content import ChallengeTrackerContent
|
3
5
|
from .challenge_tracker_detail import ChallengeTrackerDetailList
|
4
6
|
from .classes import OtfClassList
|
5
|
-
from .enums import
|
6
|
-
ALL_CLASS_STATUS,
|
7
|
-
ALL_HISTORY_CLASS_STATUS,
|
8
|
-
ALL_STUDIO_STATUS,
|
9
|
-
BookingStatus,
|
10
|
-
ChallengeType,
|
11
|
-
EquipmentType,
|
12
|
-
HistoryClassStatus,
|
13
|
-
StudioStatus,
|
14
|
-
)
|
7
|
+
from .enums import ChallengeType, EquipmentType, HistoryClassStatus
|
15
8
|
from .favorite_studios import FavoriteStudioList
|
16
9
|
from .latest_agreement import LatestAgreement
|
17
10
|
from .member_detail import MemberDetail
|
@@ -52,9 +45,8 @@ __all__ = [
|
|
52
45
|
"TelemetryMaxHr",
|
53
46
|
"StudioDetail",
|
54
47
|
"StudioDetailList",
|
55
|
-
"ALL_CLASS_STATUS",
|
56
|
-
"ALL_HISTORY_CLASS_STATUS",
|
57
|
-
"ALL_STUDIO_STATUS",
|
58
48
|
"PerformanceSummaryDetail",
|
59
49
|
"PerformanceSummaryList",
|
50
|
+
"BookClass",
|
51
|
+
"CancelBooking",
|
60
52
|
]
|
@@ -0,0 +1,304 @@
|
|
1
|
+
import inspect
|
2
|
+
from datetime import datetime
|
3
|
+
from enum import Enum
|
4
|
+
|
5
|
+
import pint
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
7
|
+
|
8
|
+
ureg = pint.UnitRegistry()
|
9
|
+
|
10
|
+
DEFAULT_WEIGHT_DIVIDERS = [55.0, 70.0, 85.0, 100.0, 115.0, 130.0, 145.0, 160.0, 175.0, 190.0, 205.0]
|
11
|
+
DEFAULT_SKELETAL_MUSCLE_MASS_DIVIDERS = [70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0]
|
12
|
+
DEFAULT_BODY_FAT_MASS_DIVIDERS = [40.0, 60.0, 80.0, 100.0, 160.0, 220.0, 280.0, 340.0, 400.0, 460.0, 520.0]
|
13
|
+
|
14
|
+
|
15
|
+
class AverageType(str, Enum):
|
16
|
+
BELOW_AVERAGE = "BELOW_AVERAGE"
|
17
|
+
AVERAGE = "AVERAGE"
|
18
|
+
ABOVE_AVERAGE = "ABOVE_AVERAGE"
|
19
|
+
MINIMUM = "MINIMUM" # unused
|
20
|
+
|
21
|
+
|
22
|
+
class BodyFatPercentIndicator(str, Enum):
|
23
|
+
NO_INDICATOR = "NO_INDICATOR"
|
24
|
+
MINIMUM_BODY_FAT = "MINIMUM_BODY_FAT" # unused
|
25
|
+
LOW_BODY_FAT = "LOW_BODY_FAT" # unused
|
26
|
+
HEALTHY_BODY_FAT = "HEALTHY_BODY_FAT"
|
27
|
+
GOAL_SETTING_FAT = "GOAL_SETTING_FAT"
|
28
|
+
HIGH_BODY_FAT = "HIGH_BODY_FAT"
|
29
|
+
OBESE_BODY_FAT = "OBESE_BODY_FAT" # unused
|
30
|
+
|
31
|
+
|
32
|
+
class Gender(str, Enum):
|
33
|
+
MALE = "M"
|
34
|
+
FEMALE = "F"
|
35
|
+
|
36
|
+
|
37
|
+
def get_percent_body_fat_descriptor(
|
38
|
+
percent_body_fat: float, body_fat_percent_dividers: list[float]
|
39
|
+
) -> BodyFatPercentIndicator:
|
40
|
+
if not percent_body_fat or not body_fat_percent_dividers[3]:
|
41
|
+
return BodyFatPercentIndicator.NO_INDICATOR
|
42
|
+
|
43
|
+
if percent_body_fat < body_fat_percent_dividers[1]:
|
44
|
+
return BodyFatPercentIndicator.HEALTHY_BODY_FAT
|
45
|
+
|
46
|
+
if percent_body_fat < body_fat_percent_dividers[2]:
|
47
|
+
return BodyFatPercentIndicator.GOAL_SETTING_FAT
|
48
|
+
|
49
|
+
return BodyFatPercentIndicator.HIGH_BODY_FAT
|
50
|
+
|
51
|
+
|
52
|
+
def get_relative_descriptor(in_body_value: float, in_body_dividers: list[float]) -> AverageType:
|
53
|
+
if in_body_value <= in_body_dividers[2]:
|
54
|
+
return AverageType.BELOW_AVERAGE
|
55
|
+
|
56
|
+
if in_body_value <= in_body_dividers[4]:
|
57
|
+
return AverageType.AVERAGE
|
58
|
+
|
59
|
+
return AverageType.ABOVE_AVERAGE
|
60
|
+
|
61
|
+
|
62
|
+
def get_body_fat_percent_dividers(age: int, gender: Gender) -> list[float]:
|
63
|
+
if gender == Gender.MALE:
|
64
|
+
return get_body_fat_percent_dividers_male(age)
|
65
|
+
|
66
|
+
return get_body_fat_percent_dividers_female(age)
|
67
|
+
|
68
|
+
|
69
|
+
def get_body_fat_percent_dividers_male(age: int) -> list[float]:
|
70
|
+
match age:
|
71
|
+
case age if 0 <= age < 30:
|
72
|
+
return [0.0, 13.1, 21.1, 100.0]
|
73
|
+
case age if 30 <= age < 40:
|
74
|
+
return [0.0, 17.1, 23.1, 100.0]
|
75
|
+
case age if 40 <= age < 50:
|
76
|
+
return [0.0, 20.1, 25.1, 100.0]
|
77
|
+
case age if 50 <= age < 60:
|
78
|
+
return [0.0, 21.1, 26.1, 100.0]
|
79
|
+
case age if 60 <= age < 70:
|
80
|
+
return [0.0, 22.1, 27.1, 100.0]
|
81
|
+
case _:
|
82
|
+
return [0.0, 0.0, 0.0, 0.0]
|
83
|
+
|
84
|
+
|
85
|
+
def get_body_fat_percent_dividers_female(age: int) -> list[float]:
|
86
|
+
match age:
|
87
|
+
case age if 0 <= age < 30:
|
88
|
+
return [0.0, 19.1, 26.1, 100.0]
|
89
|
+
case age if 30 <= age < 40:
|
90
|
+
return [0.0, 20.1, 27.1, 100.0]
|
91
|
+
case age if 40 <= age < 50:
|
92
|
+
return [0.0, 22.1, 30.1, 100.0]
|
93
|
+
case age if 50 <= age < 60:
|
94
|
+
return [0.0, 25.1, 32.1, 100.0]
|
95
|
+
case age if 60 <= age < 70:
|
96
|
+
return [0.0, 26.1, 33.1, 100.0]
|
97
|
+
case _:
|
98
|
+
return [0.0, 0.0, 0.0, 0.0]
|
99
|
+
|
100
|
+
|
101
|
+
class LeanBodyMass(BaseModel):
|
102
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
103
|
+
left_arm: float = Field(..., alias="lbmOfLeftArm")
|
104
|
+
left_leg: float = Field(..., alias="lbmOfLeftLeg")
|
105
|
+
right_arm: float = Field(..., alias="lbmOfRightArm")
|
106
|
+
right_leg: float = Field(..., alias="lbmOfRightLeg")
|
107
|
+
trunk: float = Field(..., alias="lbmOfTrunk")
|
108
|
+
|
109
|
+
|
110
|
+
class LeanBodyMassPercent(BaseModel):
|
111
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
112
|
+
left_arm: float = Field(..., alias="lbmPercentOfLeftArm")
|
113
|
+
left_leg: float = Field(..., alias="lbmPercentOfLeftLeg")
|
114
|
+
right_arm: float = Field(..., alias="lbmPercentOfRightArm")
|
115
|
+
right_leg: float = Field(..., alias="lbmPercentOfRightLeg")
|
116
|
+
trunk: float = Field(..., alias="lbmPercentOfTrunk")
|
117
|
+
|
118
|
+
|
119
|
+
class BodyFatMass(BaseModel):
|
120
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
121
|
+
control: float = Field(..., alias="bfmControl")
|
122
|
+
left_arm: float = Field(..., alias="bfmOfLeftArm")
|
123
|
+
left_leg: float = Field(..., alias="bfmOfLeftLeg")
|
124
|
+
right_arm: float = Field(..., alias="bfmOfRightArm")
|
125
|
+
right_leg: float = Field(..., alias="bfmOfRightLeg")
|
126
|
+
trunk: float = Field(..., alias="bfmOfTrunk")
|
127
|
+
|
128
|
+
|
129
|
+
class BodyFatMassPercent(BaseModel):
|
130
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
131
|
+
left_arm: float = Field(..., alias="bfmPercentOfLeftArm")
|
132
|
+
left_leg: float = Field(..., alias="bfmPercentOfLeftLeg")
|
133
|
+
right_arm: float = Field(..., alias="bfmPercentOfRightArm")
|
134
|
+
right_leg: float = Field(..., alias="bfmPercentOfRightLeg")
|
135
|
+
trunk: float = Field(..., alias="bfmPercentOfTrunk")
|
136
|
+
|
137
|
+
|
138
|
+
class TotalBodyWeight(BaseModel):
|
139
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
140
|
+
right_arm: float = Field(..., alias="tbwOfRightArm")
|
141
|
+
left_arm: float = Field(..., alias="tbwOfLeftArm")
|
142
|
+
trunk: float = Field(..., alias="tbwOfTrunk")
|
143
|
+
right_leg: float = Field(..., alias="tbwOfRightLeg")
|
144
|
+
left_leg: float = Field(..., alias="tbwOfLeftLeg")
|
145
|
+
|
146
|
+
|
147
|
+
class IntraCellularWater(BaseModel):
|
148
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
149
|
+
right_arm: float = Field(..., alias="icwOfRightArm")
|
150
|
+
left_arm: float = Field(..., alias="icwOfLeftArm")
|
151
|
+
trunk: float = Field(..., alias="icwOfTrunk")
|
152
|
+
right_leg: float = Field(..., alias="icwOfRightLeg")
|
153
|
+
left_leg: float = Field(..., alias="icwOfLeftLeg")
|
154
|
+
|
155
|
+
|
156
|
+
class ExtraCellularWater(BaseModel):
|
157
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
158
|
+
right_arm: float = Field(..., alias="ecwOfRightArm")
|
159
|
+
left_arm: float = Field(..., alias="ecwOfLeftArm")
|
160
|
+
trunk: float = Field(..., alias="ecwOfTrunk")
|
161
|
+
right_leg: float = Field(..., alias="ecwOfRightLeg")
|
162
|
+
left_leg: float = Field(..., alias="ecwOfLeftLeg")
|
163
|
+
|
164
|
+
|
165
|
+
class ExtraCellularWaterOverTotalBodyWater(BaseModel):
|
166
|
+
model_config: ConfigDict = ConfigDict(extra="ignore")
|
167
|
+
right_arm: float = Field(..., alias="ecwOverTBWOfRightArm")
|
168
|
+
left_arm: float = Field(..., alias="ecwOverTBWOfLeftArm")
|
169
|
+
trunk: float = Field(..., alias="ecwOverTBWOfTrunk")
|
170
|
+
right_leg: float = Field(..., alias="ecwOverTBWOfRightLeg")
|
171
|
+
left_leg: float = Field(..., alias="ecwOverTBWOfLeftLeg")
|
172
|
+
|
173
|
+
|
174
|
+
class BodyCompositionData(BaseModel):
|
175
|
+
member_uuid: str = Field(..., alias="memberUUId")
|
176
|
+
member_id: str = Field(..., alias="memberId")
|
177
|
+
scan_result_uuid: str = Field(..., alias="scanResultUUId")
|
178
|
+
inbody_id: str = Field(..., alias="id", exclude=True, description="InBody ID, same as email address")
|
179
|
+
email: str
|
180
|
+
height: str = Field(..., description="Height in cm")
|
181
|
+
gender: Gender
|
182
|
+
age: int
|
183
|
+
scan_datetime: datetime = Field(..., alias="testDatetime")
|
184
|
+
provided_weight: float = Field(
|
185
|
+
..., alias="weight", description="Weight in pounds, provided by member at time of scan"
|
186
|
+
)
|
187
|
+
|
188
|
+
lean_body_mass_details: LeanBodyMass
|
189
|
+
lean_body_mass_percent_details: LeanBodyMassPercent
|
190
|
+
|
191
|
+
total_body_weight: float = Field(..., alias="tbw", description="Total body weight in pounds, based on scan results")
|
192
|
+
dry_lean_mass: float = Field(..., alias="dlm")
|
193
|
+
body_fat_mass: float = Field(..., alias="bfm")
|
194
|
+
lean_body_mass: float = Field(..., alias="lbm")
|
195
|
+
skeletal_muscle_mass: float = Field(..., alias="smm")
|
196
|
+
body_mass_index: float = Field(..., alias="bmi")
|
197
|
+
percent_body_fat: float = Field(..., alias="pbf")
|
198
|
+
basal_metabolic_rate: float = Field(..., alias="bmr")
|
199
|
+
in_body_type: str = Field(..., alias="inBodyType")
|
200
|
+
|
201
|
+
body_fat_mass: float = Field(..., alias="bfm")
|
202
|
+
skeletal_muscle_mass: float = Field(..., alias="smm")
|
203
|
+
|
204
|
+
# excluded because they are only useful for end result of calculations
|
205
|
+
body_fat_mass_dividers: list[float] = Field(..., alias="bfmGraphScale", exclude=True)
|
206
|
+
body_fat_mass_plot_point: float = Field(..., alias="pfatnew", exclude=True)
|
207
|
+
skeletal_muscle_mass_dividers: list[float] = Field(..., alias="smmGraphScale", exclude=True)
|
208
|
+
skeletal_muscle_mass_plot_point: float = Field(..., alias="psmm", exclude=True)
|
209
|
+
weight_dividers: list[float] = Field(..., alias="wtGraphScale", exclude=True)
|
210
|
+
weight_plot_point: float = Field(..., alias="pwt", exclude=True)
|
211
|
+
|
212
|
+
# excluded due to 0 values
|
213
|
+
body_fat_mass_details: BodyFatMass = Field(..., exclude=True)
|
214
|
+
body_fat_mass_percent_details: BodyFatMassPercent = Field(..., exclude=True)
|
215
|
+
total_body_weight_details: TotalBodyWeight = Field(..., exclude=True)
|
216
|
+
intra_cellular_water_details: IntraCellularWater = Field(..., exclude=True)
|
217
|
+
extra_cellular_water_details: ExtraCellularWater = Field(..., exclude=True)
|
218
|
+
extra_cellular_water_over_total_body_water_details: ExtraCellularWaterOverTotalBodyWater = Field(..., exclude=True)
|
219
|
+
visceral_fat_level: float = Field(..., alias="vfl", exclude=True)
|
220
|
+
visceral_fat_area: float = Field(..., alias="vfa", exclude=True)
|
221
|
+
body_comp_measurement: float = Field(..., alias="bcm", exclude=True)
|
222
|
+
total_body_weight_over_lean_body_mass: float = Field(..., alias="tbwOverLBM", exclude=True)
|
223
|
+
intracellular_water: float = Field(..., alias="icw", exclude=True)
|
224
|
+
extracellular_water: float = Field(..., alias="ecw", exclude=True)
|
225
|
+
lean_body_mass_control: float = Field(..., alias="lbmControl", exclude=True)
|
226
|
+
|
227
|
+
def __init__(self, **data):
|
228
|
+
# populate child models
|
229
|
+
child_model_dict = {
|
230
|
+
k: v.annotation
|
231
|
+
for k, v in self.model_fields.items()
|
232
|
+
if inspect.isclass(v.annotation) and issubclass(v.annotation, BaseModel)
|
233
|
+
}
|
234
|
+
for k, v in child_model_dict.items():
|
235
|
+
data[k] = v(**data)
|
236
|
+
|
237
|
+
super().__init__(**data)
|
238
|
+
|
239
|
+
@field_validator("member_id", mode="before")
|
240
|
+
@classmethod
|
241
|
+
def int_to_str(cls, v: int):
|
242
|
+
return str(v)
|
243
|
+
|
244
|
+
@field_validator("skeletal_muscle_mass_dividers", "weight_dividers", "body_fat_mass_dividers", mode="before")
|
245
|
+
@classmethod
|
246
|
+
def convert_dividers_to_float_list(cls, v: str):
|
247
|
+
return [float(i) for i in v.split(";")]
|
248
|
+
|
249
|
+
@field_validator("total_body_weight", mode="before")
|
250
|
+
@classmethod
|
251
|
+
def convert_body_weight_from_kg_to_pounds(cls, v: float):
|
252
|
+
return ureg.Quantity(v, ureg.kilogram).to(ureg.pound).magnitude
|
253
|
+
|
254
|
+
@property
|
255
|
+
def body_fat_mass_relative_descriptor(self) -> AverageType:
|
256
|
+
"""Get the relative descriptor for the body fat mass plot point.
|
257
|
+
|
258
|
+
For this item, a lower value is better.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
AverageType: The relative descriptor for the body fat mass plot point
|
262
|
+
"""
|
263
|
+
dividers = self.body_fat_mass_dividers or DEFAULT_BODY_FAT_MASS_DIVIDERS
|
264
|
+
return get_relative_descriptor(self.body_fat_mass_plot_point, dividers)
|
265
|
+
|
266
|
+
@property
|
267
|
+
def skeletal_muscle_mass_relative_descriptor(self) -> AverageType:
|
268
|
+
"""Get the relative descriptor for the skeletal muscle mass plot point.
|
269
|
+
|
270
|
+
For this item, a higher value is better.
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
AverageType: The relative descriptor for the skeletal muscle mass plot point
|
274
|
+
|
275
|
+
"""
|
276
|
+
dividers = self.skeletal_muscle_mass_dividers or DEFAULT_SKELETAL_MUSCLE_MASS_DIVIDERS
|
277
|
+
return get_relative_descriptor(self.skeletal_muscle_mass_plot_point, dividers)
|
278
|
+
|
279
|
+
@property
|
280
|
+
def weight_relative_descriptor(self) -> AverageType:
|
281
|
+
"""Get the relative descriptor for the weight plot point.
|
282
|
+
|
283
|
+
For this item, a lower value is better.
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
AverageType: The relative descriptor for the weight
|
287
|
+
"""
|
288
|
+
dividers = self.weight_dividers or DEFAULT_WEIGHT_DIVIDERS
|
289
|
+
return get_relative_descriptor(self.weight_plot_point, dividers)
|
290
|
+
|
291
|
+
@property
|
292
|
+
def body_fat_percent_relative_descriptor(self) -> BodyFatPercentIndicator:
|
293
|
+
"""Get the relative descriptor for the percent body fat.
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
BodyFatPercentIndicator: The relative descriptor for the percent body fat
|
297
|
+
"""
|
298
|
+
return get_percent_body_fat_descriptor(
|
299
|
+
self.percent_body_fat, get_body_fat_percent_dividers(self.age, self.gender)
|
300
|
+
)
|
301
|
+
|
302
|
+
|
303
|
+
class BodyCompositionList(BaseModel):
|
304
|
+
data: list[BodyCompositionData]
|