sweatstack 0.31.1__py3-none-any.whl → 0.33.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
@@ -8,6 +8,7 @@ import time
8
8
  import urllib
9
9
  import webbrowser
10
10
  from datetime import date, datetime
11
+ from enum import Enum
11
12
  from functools import wraps
12
13
  from http.server import BaseHTTPRequestHandler, HTTPServer
13
14
  from importlib.metadata import version
@@ -20,7 +21,7 @@ import pandas as pd
20
21
 
21
22
  from .constants import DEFAULT_URL
22
23
  from .schemas import (
23
- ActivityDetails, ActivitySummary, Sport, TraceDetails, UserSummary
24
+ ActivityDetails, ActivitySummary, Metric, Sport, TraceDetails, UserSummary
24
25
  )
25
26
  from .utils import decode_jwt_body, make_dataframe_streamlit_compatible
26
27
 
@@ -114,7 +115,7 @@ class OAuth2Mixin:
114
115
  data=token_data,
115
116
  )
116
117
  try:
117
- response.raise_for_status()
118
+ self._raise_for_status(response)
118
119
  except httpx.HTTPStatusError as e:
119
120
  raise Exception(f"SweatStack Python login failed. Please try again.") from e
120
121
  token_response = response.json()
@@ -141,7 +142,7 @@ class DelegationMixin:
141
142
  "/api/v1/oauth/delegated-token",
142
143
  json={"sub": user_id},
143
144
  )
144
- response.raise_for_status()
145
+ self._raise_for_status(response)
145
146
 
146
147
  return response.json()
147
148
 
@@ -155,7 +156,7 @@ class DelegationMixin:
155
156
  response = client.get(
156
157
  "/api/v1/oauth/principal-token",
157
158
  )
158
- response.raise_for_status()
159
+ self._raise_for_status(response)
159
160
  return response.json()
160
161
 
161
162
  def switch_back(self):
@@ -206,7 +207,7 @@ class Client(OAuth2Mixin, DelegationMixin):
206
207
  },
207
208
  )
208
209
 
209
- response.raise_for_status()
210
+ self._raise_for_status(response)
210
211
  return response.json()["access_token"]
211
212
 
212
213
  def _check_token_expiry(self, token: str) -> str:
@@ -286,6 +287,15 @@ class Client(OAuth2Mixin, DelegationMixin):
286
287
  with httpx.Client(base_url=self.url, headers=headers) as client:
287
288
  yield client
288
289
 
290
+ def _raise_for_status(self, response: httpx.Response):
291
+ if response.status_code == 422:
292
+ raise ValueError(response.json())
293
+ else:
294
+ response.raise_for_status()
295
+
296
+ def _enums_to_strings(self, values: list[Enum | str]) -> list[str]:
297
+ return [value.value if isinstance(value, Enum) else value for value in values]
298
+
289
299
  def _get_activities_generator(
290
300
  self,
291
301
  *,
@@ -306,8 +316,7 @@ class Client(OAuth2Mixin, DelegationMixin):
306
316
  if end is not None:
307
317
  params["end"] = end.isoformat()
308
318
  if sports is not None:
309
- sports = [sport.value if isinstance(sport, Sport) else sport for sport in sports]
310
- params["sports"] = sports
319
+ params["sports"] = self._enums_to_strings(sports)
311
320
  if tags is not None:
312
321
  params["tags"] = tags
313
322
 
@@ -317,7 +326,7 @@ class Client(OAuth2Mixin, DelegationMixin):
317
326
  url="/api/v1/activities/",
318
327
  params=params,
319
328
  )
320
- response.raise_for_status()
329
+ self._raise_for_status(response)
321
330
  activities = response.json()
322
331
  for activity in activities:
323
332
  yield ActivitySummary.model_validate(activity)
@@ -383,7 +392,7 @@ class Client(OAuth2Mixin, DelegationMixin):
383
392
  def get_activity(self, activity_id: str) -> ActivityDetails:
384
393
  with self._http_client() as client:
385
394
  response = client.get(url=f"/api/v1/activities/{activity_id}")
386
- response.raise_for_status()
395
+ self._raise_for_status(response)
387
396
  return ActivityDetails.model_validate(response.json())
388
397
 
389
398
  def get_activity_data(
@@ -400,8 +409,7 @@ class Client(OAuth2Mixin, DelegationMixin):
400
409
  url=f"/api/v1/activities/{activity_id}/data",
401
410
  params=params,
402
411
  )
403
-
404
- response.raise_for_status()
412
+ self._raise_for_status(response)
405
413
 
406
414
  df = pd.read_parquet(BytesIO(response.content))
407
415
  return self._postprocess_dataframe(df)
@@ -409,9 +417,10 @@ class Client(OAuth2Mixin, DelegationMixin):
409
417
  def get_activity_mean_max(
410
418
  self,
411
419
  activity_id: str,
412
- metric: str,
420
+ metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
413
421
  adaptive_sampling: bool = False,
414
422
  ) -> pd.DataFrame:
423
+ metric = self._enums_to_strings([metric])[0]
415
424
  with self._http_client() as client:
416
425
  response = client.get(
417
426
  url=f"/api/v1/activities/{activity_id}/mean-max",
@@ -420,13 +429,13 @@ class Client(OAuth2Mixin, DelegationMixin):
420
429
  "adaptive_sampling": adaptive_sampling,
421
430
  },
422
431
  )
423
- response.raise_for_status()
432
+ self._raise_for_status(response)
424
433
  df = pd.read_parquet(BytesIO(response.content))
425
434
  return self._postprocess_dataframe(df)
426
435
 
427
436
  def get_latest_activity_data(
428
437
  self,
429
- sport: Sport | None = None,
438
+ sport: Sport | str | None = None,
430
439
  adaptive_sampling_on: Literal["power", "speed"] | None = None,
431
440
  ) -> pd.DataFrame:
432
441
  activity = self.get_latest_activity(sport=sport)
@@ -434,8 +443,8 @@ class Client(OAuth2Mixin, DelegationMixin):
434
443
 
435
444
  def get_latest_activity_mean_max(
436
445
  self,
437
- metric: str,
438
- sport: Sport | None = None,
446
+ metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
447
+ sport: Sport | str | None = None,
439
448
  adaptive_sampling: bool = False,
440
449
  ) -> pd.DataFrame:
441
450
  activity = self.get_latest_activity(sport=sport)
@@ -444,11 +453,11 @@ class Client(OAuth2Mixin, DelegationMixin):
444
453
  def get_longitudinal_data(
445
454
  self,
446
455
  *,
447
- sport: Sport | None = None,
456
+ sport: Sport | str | None = None,
448
457
  sports: list[Sport | str] | None = None,
449
458
  start: date | str,
450
459
  end: date | str | None = None,
451
- metrics: list[str] | None = None,
460
+ metrics: list[Metric | str] | None = None,
452
461
  adaptive_sampling_on: Literal["power", "speed"] | None = None,
453
462
  ) -> pd.DataFrame:
454
463
  if sport and sports:
@@ -458,6 +467,9 @@ class Client(OAuth2Mixin, DelegationMixin):
458
467
  elif sports is None:
459
468
  sports = []
460
469
 
470
+ sports = self._enums_to_strings(sports)
471
+ metrics = self._enums_to_strings(metrics)
472
+
461
473
  params = {
462
474
  "sports": sports,
463
475
  "start": start,
@@ -474,7 +486,7 @@ class Client(OAuth2Mixin, DelegationMixin):
474
486
  url="/api/v1/activities/longitudinal-data",
475
487
  params=params,
476
488
  )
477
- response.raise_for_status()
489
+ self._raise_for_status(response)
478
490
 
479
491
  df = pd.read_parquet(BytesIO(response.content))
480
492
  return self._postprocess_dataframe(df)
@@ -483,10 +495,13 @@ class Client(OAuth2Mixin, DelegationMixin):
483
495
  self,
484
496
  *,
485
497
  sport: Sport | str,
486
- metric: str,
498
+ metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
487
499
  date: date | str | None = None,
488
500
  window_days: int | None = None,
489
501
  ) -> pd.DataFrame:
502
+ sport = self._enums_to_strings([sport])[0]
503
+ metric = self._enums_to_strings([metric])[0]
504
+
490
505
  params = {
491
506
  "sport": sport,
492
507
  "metric": metric,
@@ -501,7 +516,7 @@ class Client(OAuth2Mixin, DelegationMixin):
501
516
  url="/api/v1/activities/longitudinal-mean-max",
502
517
  params=params,
503
518
  )
504
- response.raise_for_status()
519
+ self._raise_for_status(response)
505
520
 
506
521
  df = pd.read_parquet(BytesIO(response.content))
507
522
  return self._postprocess_dataframe(df)
@@ -526,8 +541,7 @@ class Client(OAuth2Mixin, DelegationMixin):
526
541
  if end is not None:
527
542
  params["end"] = end.isoformat()
528
543
  if sports is not None:
529
- sports = [sport.value if isinstance(sport, Sport) else sport for sport in sports]
530
- params["sports"] = sports
544
+ params["sports"] = self._enums_to_strings(sports)
531
545
  if tags is not None:
532
546
  params["tags"] = tags
533
547
 
@@ -537,7 +551,7 @@ class Client(OAuth2Mixin, DelegationMixin):
537
551
  url="/api/v1/traces/",
538
552
  params=params,
539
553
  )
540
- response.raise_for_status()
554
+ self._raise_for_status(response)
541
555
  traces = response.json()
542
556
  for trace in traces:
543
557
  yield TraceDetails.model_validate(trace)
@@ -636,7 +650,7 @@ class Client(OAuth2Mixin, DelegationMixin):
636
650
  "tags": tags,
637
651
  },
638
652
  )
639
- response.raise_for_status()
653
+ self._raise_for_status(response)
640
654
  return TraceDetails.model_validate(response.json())
641
655
 
642
656
  def get_sports(self, only_root: bool = False) -> list[Sport]:
@@ -645,7 +659,7 @@ class Client(OAuth2Mixin, DelegationMixin):
645
659
  url="/api/v1/profile/sports/",
646
660
  params={"only_root": only_root},
647
661
  )
648
- response.raise_for_status()
662
+ self._raise_for_status(response)
649
663
  return [Sport(sport) for sport in response.json()]
650
664
 
651
665
  def get_tags(self) -> list[str]:
@@ -653,7 +667,7 @@ class Client(OAuth2Mixin, DelegationMixin):
653
667
  response = client.get(
654
668
  url="/api/v1/profile/tags/",
655
669
  )
656
- response.raise_for_status()
670
+ self._raise_for_status(response)
657
671
  return response.json()
658
672
 
659
673
  def get_users(self) -> list[UserSummary]:
@@ -661,7 +675,7 @@ class Client(OAuth2Mixin, DelegationMixin):
661
675
  response = client.get(
662
676
  url="/api/v1/users/",
663
677
  )
664
- response.raise_for_status()
678
+ self._raise_for_status(response)
665
679
  return [UserSummary.model_validate(user) for user in response.json()]
666
680
 
667
681
  _default_client = Client()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.31.1
3
+ Version: 0.33.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,6 +1,6 @@
1
1
  sweatstack/__init__.py,sha256=tiVfgKlswRPaDMEy0gA7u8rveqEYZTA_kyB9lJ3J6Sc,21
2
2
  sweatstack/cli.py,sha256=N1NWOgEZR2yaJvIXxo9qvp_jFlypZYb0nujpbVNYQ6A,720
3
- sweatstack/client.py,sha256=jzTjKcgfooADa9JUqKJPV2Z-HbAmNZOeqgVkk6Y89eU,24106
3
+ sweatstack/client.py,sha256=NpZjqDION-cets4B87brCL3sFMBAuNiP9_HKc9l6Cy4,24898
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
@@ -11,7 +11,7 @@ sweatstack/streamlit.py,sha256=gsgiIDW-INGTvF24ANnX5LJ17ZxnvXx95sjSmtcTlnY,8062
11
11
  sweatstack/sweatshell.py,sha256=MYLNcWbOdceqKJ3S0Pe8dwHXEeYsGJNjQoYUXpMTftA,333
12
12
  sweatstack/utils.py,sha256=3K97OSWQy5KZ97QiBbW4Kx1wDGxwyA96ZWpwIbkcQZc,2090
13
13
  sweatstack/Sweat Stack examples/Getting started.ipynb,sha256=k2hiSffWecoQ0VxjdpDcgFzBXDQiYEebhnAYlu8cgX8,6335204
14
- sweatstack-0.31.1.dist-info/METADATA,sha256=GCb3Q6kmp5d74Ap6FFGAeR5o14MfqmcE3773VsEWM3A,775
15
- sweatstack-0.31.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- sweatstack-0.31.1.dist-info/entry_points.txt,sha256=kCzOUQI3dqbTpEYqtgYDeiKFaqaA7BMlV6D24BMzCFU,208
17
- sweatstack-0.31.1.dist-info/RECORD,,
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,,