otf-api 0.9.1__py3-none-any.whl → 0.9.2__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 CHANGED
@@ -4,7 +4,7 @@ from otf_api.api import Otf
4
4
  from otf_api import models
5
5
  from otf_api.auth import OtfUser
6
6
 
7
- __version__ = "0.9.1"
7
+ __version__ = "0.9.2"
8
8
 
9
9
 
10
10
  __all__ = ["Otf", "OtfUser", "models"]
otf_api/api.py CHANGED
@@ -87,9 +87,7 @@ class Otf:
87
87
  LOGGER.exception(f"Response: {response.text}")
88
88
  raise
89
89
  except httpx.HTTPStatusError as e:
90
- LOGGER.exception(f"Error making request: {e}")
91
- LOGGER.exception(f"Response: {response.text}")
92
- raise exc.OtfRequestError("Error making request", response=response, request=request)
90
+ raise exc.OtfRequestError("Error making request", e, response=response, request=request)
93
91
  except Exception as e:
94
92
  LOGGER.exception(f"Error making request: {e}")
95
93
  raise
@@ -110,7 +108,7 @@ class Otf:
110
108
  and not (resp["Status"] >= 200 and resp["Status"] <= 299)
111
109
  ):
112
110
  LOGGER.error(f"Error making request: {resp}")
113
- raise exc.OtfRequestError("Error making request", response=response, request=request)
111
+ raise exc.OtfRequestError("Error making request", None, response=response, request=request)
114
112
 
115
113
  return resp
116
114
 
@@ -966,6 +964,7 @@ class Otf:
966
964
 
967
965
  path = f"/v1/performance-summaries/{performance_summary_id}"
968
966
  res = self._performance_summary_request("GET", path)
967
+
969
968
  if res is None:
970
969
  raise exc.ResourceNotFoundError(f"Performance summary {performance_summary_id} not found")
971
970
 
@@ -1147,6 +1146,137 @@ class Otf:
1147
1146
 
1148
1147
  return models.MemberDetail(**res["data"])
1149
1148
 
1149
+ def _rate_class(
1150
+ self,
1151
+ class_uuid: str,
1152
+ class_history_uuid: str,
1153
+ class_rating: Literal[0, 1, 2, 3],
1154
+ coach_rating: Literal[0, 1, 2, 3],
1155
+ ) -> models.PerformanceSummaryEntry:
1156
+ """Rate a class and coach. A simpler method is provided in `rate_class_from_performance_summary`.
1157
+
1158
+
1159
+ The class rating must be between 0 and 4.
1160
+ 0 is the same as dismissing the prompt to rate the class/coach in the app.
1161
+ 1 through 3 is a range from bad to good.
1162
+
1163
+ Args:
1164
+ class_uuid (str): The class UUID.
1165
+ class_history_uuid (str): The performance summary ID.
1166
+ class_rating (int): The class rating. Must be 0, 1, 2, or 3.
1167
+ coach_rating (int): The coach rating. Must be 0, 1, 2, or 3.
1168
+
1169
+ Returns:
1170
+ PerformanceSummaryEntry: The updated performance summary entry.
1171
+ """
1172
+
1173
+ # com/orangetheoryfitness/fragment/rating/RateStatus.java
1174
+
1175
+ # we convert these to the new values that the app uses
1176
+ # mainly because we don't want to cause any issues with the API and/or with OTF corporate
1177
+ # wondering where the old values are coming from
1178
+
1179
+ COACH_RATING_MAP = {0: 0, 1: 16, 2: 17, 3: 18}
1180
+ CLASS_RATING_MAP = {0: 0, 1: 19, 2: 20, 3: 21}
1181
+
1182
+ if class_rating not in CLASS_RATING_MAP:
1183
+ raise ValueError(f"Invalid class rating {class_rating}")
1184
+
1185
+ if coach_rating not in COACH_RATING_MAP:
1186
+ raise ValueError(f"Invalid coach rating {coach_rating}")
1187
+
1188
+ body_class_rating = CLASS_RATING_MAP[class_rating]
1189
+ body_coach_rating = COACH_RATING_MAP[coach_rating]
1190
+
1191
+ body = {
1192
+ "classUUId": class_uuid,
1193
+ "otBeatClassHistoryUUId": class_history_uuid,
1194
+ "classRating": body_class_rating,
1195
+ "coachRating": body_coach_rating,
1196
+ }
1197
+
1198
+ try:
1199
+ self._default_request("POST", "/mobile/v1/members/classes/ratings", json=body)
1200
+ except exc.OtfRequestError as e:
1201
+ if e.response.status_code == 403:
1202
+ raise exc.AlreadyRatedError(f"Performance summary {class_history_uuid} is already rated.") from None
1203
+ raise
1204
+
1205
+ return self._get_performance_summary_entry_from_id(class_history_uuid)
1206
+
1207
+ def _get_performance_summary_entry_from_id(self, class_history_uuid: str) -> models.PerformanceSummaryEntry:
1208
+ """Get a performance summary entry from the ID.
1209
+
1210
+ This is a helper function to compensate for the fact that a PerformanceSummaryDetail object does not contain
1211
+ the class UUID, which is required to rate the class. It will also be used to return an updated performance
1212
+ summary entry after rating a class.
1213
+
1214
+ Args:
1215
+ class_history_uuid (str): The performance summary ID.
1216
+
1217
+ Returns:
1218
+ PerformanceSummaryEntry: The performance summary entry.
1219
+
1220
+ Raises:
1221
+ ResourceNotFoundError: If the performance summary is not found.
1222
+ """
1223
+
1224
+ # try going in as small of increments as possible, assuming that the rating request
1225
+ # will be for a recent class
1226
+ for limit in [5, 20, 60, 100]:
1227
+ summaries = self.get_performance_summaries(limit)
1228
+ summary = next((s for s in summaries if s.class_history_uuid == class_history_uuid), None)
1229
+
1230
+ if summary:
1231
+ return summary
1232
+
1233
+ raise exc.ResourceNotFoundError(f"Performance summary {class_history_uuid} not found.")
1234
+
1235
+ def rate_class_from_performance_summary(
1236
+ self,
1237
+ perf_summary: models.PerformanceSummaryEntry | models.PerformanceSummaryDetail,
1238
+ class_rating: Literal[0, 1, 2, 3],
1239
+ coach_rating: Literal[0, 1, 2, 3],
1240
+ ) -> models.PerformanceSummaryEntry:
1241
+ """Rate a class and coach. The class rating must be 0, 1, 2, or 3. 0 is the same as dismissing the prompt to
1242
+ rate the class/coach. 1 - 3 is a range from bad to good.
1243
+
1244
+ Args:
1245
+ perf_summary (PerformanceSummaryEntry): The performance summary entry to rate.
1246
+ class_rating (int): The class rating. Must be 0, 1, 2, or 3.
1247
+ coach_rating (int): The coach rating. Must be 0, 1, 2, or 3.
1248
+
1249
+ Returns:
1250
+ PerformanceSummaryEntry: The updated performance summary entry.
1251
+
1252
+ Raises:
1253
+ ValueError: If `perf_summary` is not a PerformanceSummaryEntry.
1254
+ AlreadyRatedError: If the performance summary is already rated.
1255
+ ClassNotRatableError: If the performance summary is not rateable.
1256
+ ValueError: If the performance summary does not have an associated class.
1257
+ """
1258
+
1259
+ if isinstance(perf_summary, models.PerformanceSummaryDetail):
1260
+ perf_summary = self._get_performance_summary_entry_from_id(perf_summary.class_history_uuid)
1261
+
1262
+ if not isinstance(perf_summary, models.PerformanceSummaryEntry):
1263
+ raise ValueError(f"`perf_summary` must be a PerformanceSummaryEntry, got {type(perf_summary)}")
1264
+
1265
+ if perf_summary.is_rated:
1266
+ raise exc.AlreadyRatedError(f"Performance summary {perf_summary.class_history_uuid} is already rated.")
1267
+
1268
+ if not perf_summary.ratable:
1269
+ raise exc.ClassNotRatableError(f"Performance summary {perf_summary.class_history_uuid} is not rateable.")
1270
+
1271
+ if not perf_summary.otf_class or not perf_summary.otf_class.class_uuid:
1272
+ raise ValueError(
1273
+ f"Performance summary {perf_summary.class_history_uuid} does not have an associated class."
1274
+ )
1275
+
1276
+ return self._rate_class(
1277
+ perf_summary.otf_class.class_uuid, perf_summary.class_history_uuid, class_rating, coach_rating
1278
+ )
1279
+
1150
1280
  # the below do not return any data for me, so I can't test them
1151
1281
 
1152
1282
  def _get_member_services(self, active_only: bool = True) -> Any:
otf_api/exceptions.py CHANGED
@@ -8,11 +8,13 @@ class OtfException(Exception):
8
8
  class OtfRequestError(OtfException):
9
9
  """Raised when an error occurs while making a request to the OTF API."""
10
10
 
11
+ original_exception: Exception
11
12
  response: Response
12
13
  request: Request
13
14
 
14
- def __init__(self, message: str, response: Response, request: Request):
15
+ def __init__(self, message: str, original_exception: Exception | None, response: Response, request: Request):
15
16
  super().__init__(message)
17
+ self.original_exception = original_exception
16
18
  self.response = response
17
19
  self.request = request
18
20
 
@@ -49,3 +51,11 @@ class BookingNotFoundError(OtfException):
49
51
 
50
52
  class ResourceNotFoundError(OtfException):
51
53
  """Raised when a resource is not found."""
54
+
55
+
56
+ class AlreadyRatedError(OtfException):
57
+ """Raised when attempting to rate a class that is already rated."""
58
+
59
+
60
+ class ClassNotRatableError(OtfException):
61
+ """Raised when attempting to rate a class that is not ratable."""
@@ -68,11 +68,16 @@ class Rower(BaseEquipment):
68
68
 
69
69
 
70
70
  class PerformanceSummaryDetail(OtfItemBase):
71
- id: str
71
+ class_history_uuid: str = Field(..., alias="id")
72
72
  class_name: str | None = Field(None, alias=AliasPath("class", "name"))
73
73
  class_starts_at: datetime | None = Field(None, alias=AliasPath("class", "starts_at_local"))
74
74
 
75
- ratable: bool | None = None
75
+ ratable: bool | None = Field(
76
+ None,
77
+ exclude=True,
78
+ repr=False,
79
+ description="Seems to be inaccurate, not reflecting ratable from `PerformanceSummaryEntry`",
80
+ )
76
81
  calories_burned: int | None = Field(None, alias=AliasPath("details", "calories_burned"))
77
82
  splat_points: int | None = Field(None, alias=AliasPath("details", "splat_points"))
78
83
  step_count: int | None = Field(None, alias=AliasPath("details", "step_count"))
@@ -33,7 +33,7 @@ class ClassRating(OtfItemBase):
33
33
 
34
34
 
35
35
  class PerformanceSummaryEntry(OtfItemBase):
36
- id: str = Field(..., alias="id")
36
+ class_history_uuid: str = Field(..., alias="id")
37
37
  calories_burned: int | None = Field(None, alias=AliasPath("details", "calories_burned"))
38
38
  splat_points: int | None = Field(None, alias=AliasPath("details", "splat_points"))
39
39
  step_count: int | None = Field(None, alias=AliasPath("details", "step_count"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: otf-api
3
- Version: 0.9.1
3
+ Version: 0.9.2
4
4
  Summary: Python OrangeTheory Fitness API Client
5
5
  License: MIT
6
6
  Author: Jessica Smith
@@ -1,10 +1,10 @@
1
- otf_api/__init__.py,sha256=dGgyclSo165V_hZAl7Z5E06WQrs5NjO6fXtPcbo3RIg,204
2
- otf_api/api.py,sha256=Ah6bheezsV-RLxPLGKZqqW77gotWkznLYmEeRhEwJaY,47006
1
+ otf_api/__init__.py,sha256=QDClynfqFpgl8WPwfIe7AGh9UKPBrhj081fXluLF4-k,204
2
+ otf_api/api.py,sha256=UNuMgJ6RnEj1rU8V6Funtnv3WQlxp2ShOJKCkfI-iC0,52570
3
3
  otf_api/auth/__init__.py,sha256=Pu6ugbEXhgjJEpYKr7K2CqEac53Wq9FRF3F16GLQ_ww,133
4
4
  otf_api/auth/auth.py,sha256=m9qzUn0hdAo6AZHm2Mn054nLqUWPxEyP3y3DOBVkvYA,9010
5
5
  otf_api/auth/user.py,sha256=HgggfneeVMnKFWSUMnlikfZyC__HE8vVaKSpBS4_sIg,2256
6
6
  otf_api/auth/utils.py,sha256=jUH_A1-DU3KfY-XrymCuQoud79o9qfu5P9Ws9QA2aWs,3211
7
- otf_api/exceptions.py,sha256=mt0k0-2oNcpNhboYzkywpt535W6bHjx6gro7P3PpYzo,1463
7
+ otf_api/exceptions.py,sha256=GISekwF5dPt0Ol0WCU55kE5ODc5VxicNEEhmlguuE0U,1815
8
8
  otf_api/filters.py,sha256=fk2bFGi3srjS96qZlaDx-ARZRaj93NUTUdMJ01TX420,3702
9
9
  otf_api/logging.py,sha256=PRZpCaJ1F1Xya3L9Efkt3mKS5_QNr3sXjEUERSxYjvE,563
10
10
  otf_api/models/__init__.py,sha256=czt_OYsU4igeudRAi8kqOQro6fjNrbE9aLr--FA76Sg,1588
@@ -22,14 +22,14 @@ otf_api/models/member_purchases.py,sha256=Ne7ByEbGTqTJhuEyCgywWe8I3nc-D46qw09up7
22
22
  otf_api/models/mixins.py,sha256=VR5EeweySHhzaiqqnCr853Cpe1uK97cxY0IFEdf5T8w,2262
23
23
  otf_api/models/notifications.py,sha256=AkmIfiIiU6wox_7puyenbhCX10SFvBD5eBAovcurRgY,833
24
24
  otf_api/models/out_of_studio_workout_history.py,sha256=El5i0K2Td_sMReyfUKP-Iv1L1WgRx0ijjjnHzYvlCeY,1703
25
- otf_api/models/performance_summary_detail.py,sha256=HmtAJIKkekYy2rJOQBxfQGsKJxuFZm7gP4Dauae-PqM,2711
26
- otf_api/models/performance_summary_list.py,sha256=jv28jZJEOXOZtghhHhNR-4BPVBY3TpyiwHNKmnoTRcA,1692
25
+ otf_api/models/performance_summary_detail.py,sha256=U-Z-6-1rJHhtMuzCAnKZtAE0VPRCPUB8IzldINI8JqA,2917
26
+ otf_api/models/performance_summary_list.py,sha256=ydOVYRwdI_U0yxW4USrDQilLafwxMzUy80AHDmJvmdQ,1708
27
27
  otf_api/models/studio_detail.py,sha256=4HZXP6khjuFs7J7lokr3rMEDDpCwcb7OYJVJvzgly7U,3639
28
28
  otf_api/models/studio_services.py,sha256=aGLQMQmjGVpI6YxzAl-mcp3Y9cHPXuH9dIqrl6E-78E,1665
29
29
  otf_api/models/telemetry.py,sha256=_g-wtJDbaXENk1DqH0bbpJUsGepWDYraRIsIvpvDBU8,2531
30
30
  otf_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  otf_api/utils.py,sha256=Af_Xg2QLoFqRzHOj4LtCIIqpRsdih-7HW1rJ8tUo8tE,3937
32
- otf_api-0.9.1.dist-info/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
33
- otf_api-0.9.1.dist-info/METADATA,sha256=SPUE5dzWt65jmeoSiWbCY-Tb84vO-x6kOg91kKlvlmg,2296
34
- otf_api-0.9.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
35
- otf_api-0.9.1.dist-info/RECORD,,
32
+ otf_api-0.9.2.dist-info/LICENSE,sha256=UaPT9ynYigC3nX8n22_rC37n-qmTRKLFaHrtUwF9ktE,1071
33
+ otf_api-0.9.2.dist-info/METADATA,sha256=VA_ZhzHmo4mZMLFaTP1YD94N04xJXeQG29n32chePaE,2296
34
+ otf_api-0.9.2.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
35
+ otf_api-0.9.2.dist-info/RECORD,,