sweatstack 0.70.0__tar.gz → 0.71.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 (61) hide show
  1. {sweatstack-0.70.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/client.md +14 -6
  2. {sweatstack-0.70.0 → sweatstack-0.71.0}/CHANGELOG.md +14 -0
  3. {sweatstack-0.70.0 → sweatstack-0.71.0}/PKG-INFO +1 -1
  4. {sweatstack-0.70.0 → sweatstack-0.71.0}/pyproject.toml +1 -1
  5. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/client.py +92 -42
  6. {sweatstack-0.70.0 → sweatstack-0.71.0}/.claude/settings.local.json +0 -0
  7. {sweatstack-0.70.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  8. {sweatstack-0.70.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
  9. {sweatstack-0.70.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
  10. {sweatstack-0.70.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  11. {sweatstack-0.70.0 → sweatstack-0.71.0}/.gitignore +0 -0
  12. {sweatstack-0.70.0 → sweatstack-0.71.0}/.python-version +0 -0
  13. {sweatstack-0.70.0 → sweatstack-0.71.0}/CLIENT_DTYPE_CONVERSION.md +0 -0
  14. {sweatstack-0.70.0 → sweatstack-0.71.0}/CLIENT_LIBRARY_SKILL.md +0 -0
  15. {sweatstack-0.70.0 → sweatstack-0.71.0}/DEVELOPMENT.md +0 -0
  16. {sweatstack-0.70.0 → sweatstack-0.71.0}/FASTAPI_DOCS.md +0 -0
  17. {sweatstack-0.70.0 → sweatstack-0.71.0}/FASTAPI_PLUGIN.md +0 -0
  18. {sweatstack-0.70.0 → sweatstack-0.71.0}/FASTAPI_USER_SWITCHING.md +0 -0
  19. {sweatstack-0.70.0 → sweatstack-0.71.0}/FASTAPI_WEBHOOKS.md +0 -0
  20. {sweatstack-0.70.0 → sweatstack-0.71.0}/LOCAL_AUTH.md +0 -0
  21. {sweatstack-0.70.0 → sweatstack-0.71.0}/Makefile +0 -0
  22. {sweatstack-0.70.0 → sweatstack-0.71.0}/README.md +0 -0
  23. {sweatstack-0.70.0 → sweatstack-0.71.0}/docs/conf.py +0 -0
  24. {sweatstack-0.70.0 → sweatstack-0.71.0}/docs/everything.rst +0 -0
  25. {sweatstack-0.70.0 → sweatstack-0.71.0}/docs/index.rst +0 -0
  26. {sweatstack-0.70.0 → sweatstack-0.71.0}/examples/fastapi_webhooks_example.py +0 -0
  27. {sweatstack-0.70.0 → sweatstack-0.71.0}/examples/send_webhook.py +0 -0
  28. {sweatstack-0.70.0 → sweatstack-0.71.0}/examples/tokens.db +0 -0
  29. {sweatstack-0.70.0 → sweatstack-0.71.0}/fastapi_coaching_example.py +0 -0
  30. {sweatstack-0.70.0 → sweatstack-0.71.0}/fastapi_example.py +0 -0
  31. {sweatstack-0.70.0 → sweatstack-0.71.0}/fastapi_sweatstack.py +0 -0
  32. {sweatstack-0.70.0 → sweatstack-0.71.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  33. {sweatstack-0.70.0 → sweatstack-0.71.0}/playground/README.md +0 -0
  34. {sweatstack-0.70.0 → sweatstack-0.71.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  35. {sweatstack-0.70.0 → sweatstack-0.71.0}/playground/Untitled.ipynb +0 -0
  36. {sweatstack-0.70.0 → sweatstack-0.71.0}/playground/hello.py +0 -0
  37. {sweatstack-0.70.0 → sweatstack-0.71.0}/playground/pyproject.toml +0 -0
  38. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  39. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/__init__.py +0 -0
  40. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/cli.py +0 -0
  41. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/constants.py +0 -0
  42. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/__init__.py +0 -0
  43. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/config.py +0 -0
  44. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/dependencies.py +0 -0
  45. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/models.py +0 -0
  46. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/routes.py +0 -0
  47. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/session.py +0 -0
  48. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  49. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  50. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/ipython_init.py +0 -0
  51. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  52. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/openapi_schemas.py +0 -0
  53. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/py.typed +0 -0
  54. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/schemas.py +0 -0
  55. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/streamlit.py +0 -0
  56. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/sweatshell.py +0 -0
  57. {sweatstack-0.70.0 → sweatstack-0.71.0}/src/sweatstack/utils.py +0 -0
  58. {sweatstack-0.70.0 → sweatstack-0.71.0}/tests/__init__.py +0 -0
  59. {sweatstack-0.70.0 → sweatstack-0.71.0}/tests/test_dtype_conversion.py +0 -0
  60. {sweatstack-0.70.0 → sweatstack-0.71.0}/tests/test_webhooks.py +0 -0
  61. {sweatstack-0.70.0 → sweatstack-0.71.0}/uv.lock +0 -0
@@ -102,23 +102,31 @@ Aggregated time-series across multiple activities. One request instead of loopin
102
102
 
103
103
  ```python
104
104
  df = client.get_longitudinal_data(
105
- sport=Sport.cycling, # single sport
105
+ sports=[Sport.cycling_road], # list of Sport enum or strings
106
106
  start=date(2025, 1, 1), # required
107
107
  end=date(2025, 12, 31), # optional (defaults to today)
108
108
  metrics=[Metric.power, Metric.heart_rate], # optional
109
109
  )
110
110
 
111
- # Or filter by multiple sports
111
+ # Multiple sports
112
112
  df = client.get_longitudinal_data(
113
113
  sports=[Sport.cycling_road, Sport.cycling_gravel],
114
114
  start=date(2025, 1, 1),
115
115
  )
116
116
 
117
117
  # Longitudinal mean-max (best efforts across time range)
118
- df = client.get_longitudinal_mean_max(sport=Sport.cycling, start=date(2025, 1, 1))
118
+ df = client.get_longitudinal_mean_max(
119
+ sports=[Sport.cycling_road],
120
+ metric="power",
121
+ start=date(2025, 1, 1),
122
+ )
119
123
 
120
124
  # Longitudinal AWD
121
- df = client.get_longitudinal_awd(sport=Sport.cycling, start=date(2025, 1, 1))
125
+ df = client.get_longitudinal_awd(
126
+ sports=[Sport.cycling_road],
127
+ metric="power",
128
+ start=date(2025, 1, 1),
129
+ )
122
130
  ```
123
131
 
124
132
  The DataFrame has a timezone-aware datetime index and includes an `activity_id` column — group by it for per-activity aggregation.
@@ -128,7 +136,7 @@ The DataFrame has a timezone-aware datetime index and includes an `activity_id`
128
136
  import os
129
137
  os.environ["SWEATSTACK_LOCAL_CACHE"] = "true"
130
138
  # Use fixed end dates (not "today") to get stable cache hits
131
- df = client.get_longitudinal_data(sport=Sport.cycling, start=date(2025, 1, 1), end=date(2025, 3, 31))
139
+ df = client.get_longitudinal_data(sports=[Sport.cycling], start=date(2025, 1, 1), end=date(2025, 3, 31))
132
140
  ```
133
141
 
134
142
  ## Traces
@@ -252,7 +260,7 @@ df = sweatstack.get_latest_activity_data()
252
260
 
253
261
  - **Sport enum uses underscores:** `Sport.cycling_road`, not `Sport("road")` or `Sport.cycling.road`. String values use dots: `"cycling.road"`.
254
262
  - **`start` is required for longitudinal endpoints.** Unlike `get_activities()` where all filters are optional.
255
- - **`sport` (singular) vs `sports` (list):** `get_latest_activity(sport=...)` takes one. `get_activities(sports=[...])` and `get_longitudinal_data(sports=[...])` take a list.
263
+ - **`sport` (singular) vs `sports` (list):** `get_latest_activity(sport=...)` and `create_trace(sport=...)` take a single sport. All other methods that filter by sport use `sports=[...]` (list). The singular `sport` parameter on longitudinal methods is deprecated.
256
264
  - **DataFrames have standard dtypes.** The library converts API-optimized types (Int16, float16) to float64/datetime64[ns] automatically.
257
265
  - **`as_dataframe=True`** is available on `get_activities()` and `get_traces()`. Time-series methods (`get_activity_data`, `get_longitudinal_data`, etc.) always return DataFrames.
258
266
  - **`summary` fields are optional.** Always null-check: `activity.summary.power.mean if activity.summary and activity.summary.power else None`.
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [0.71.0] - 2026-03-13
10
+
11
+ ### Added
12
+ - `sports` (list) parameter on `get_longitudinal_mean_max()` and `get_longitudinal_awd()` for multi-sport support. The existing `sport` (single) parameter remains for backwards compatibility.
13
+ - `start` and `end` date-range parameters on `get_longitudinal_mean_max()` and `get_longitudinal_awd()`.
14
+
15
+ ### Changed
16
+ - `get_activities()`, `get_traces()`, and `get_longitudinal_data()` now send the `sport` query key to the API instead of `sports`.
17
+
18
+ ### Deprecated
19
+ - `sport` (singular) parameter on `get_longitudinal_mean_max()`, `get_longitudinal_awd()`, and `get_longitudinal_data()`. Use `sports` (list) instead.
20
+ - `date` and `window_days` parameters on `get_longitudinal_mean_max()` and `get_longitudinal_awd()`. Use `start`/`end` instead.
21
+
22
+
9
23
  ## [0.70.0] - 2026-03-13
10
24
 
11
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.70.0
3
+ Version: 0.71.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
  [project]
2
2
  name = "sweatstack"
3
- version = "0.70.0"
3
+ version = "0.71.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -10,6 +10,7 @@ import shutil
10
10
  import tempfile
11
11
  import time
12
12
  import urllib
13
+ import warnings
13
14
  import webbrowser
14
15
  from datetime import date, datetime
15
16
  from enum import Enum
@@ -1002,7 +1003,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1002
1003
  if end is not None:
1003
1004
  params["end"] = end.isoformat()
1004
1005
  if sports is not None:
1005
- params["sports"] = self._enums_to_strings(sports)
1006
+ params["sport"] = self._enums_to_strings(sports)
1006
1007
  if tags is not None:
1007
1008
  params["tags"] = tags
1008
1009
 
@@ -1336,8 +1337,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1336
1337
  def get_longitudinal_data(
1337
1338
  self,
1338
1339
  *,
1339
- sport: Sport | str | None = None,
1340
1340
  sports: list[Sport | str] | None = None,
1341
+ sport: Sport | str | None = None,
1341
1342
  start: date | str,
1342
1343
  end: date | str | None = None,
1343
1344
  metrics: list[Metric | str] | None = None,
@@ -1349,10 +1350,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1349
1350
  including sport type and date range. The data is returned as a pandas DataFrame.
1350
1351
 
1351
1352
  Args:
1352
- sport: Optional single sport to filter by. Can be a Sport enum or string.
1353
- Cannot be used together with 'sports'.
1354
1353
  sports: Optional list of sports to filter by. Can be a list of Sport enums or strings.
1355
- Cannot be used together with 'sport'.
1354
+ sport: Deprecated. Use ``sports`` instead.
1356
1355
  start: The start date for the data range. Can be a date object or string in ISO format.
1357
1356
  end: Optional end date for the data range. Can be a date object or string in ISO format.
1358
1357
  metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
@@ -1366,15 +1365,19 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1366
1365
  ValueError: If both 'sport' and 'sports' parameters are provided.
1367
1366
  HTTPStatusError: If the API request fails.
1368
1367
  """
1369
- if sport and sports:
1370
- raise ValueError("Cannot specify both sport and sports")
1368
+ if sport is not None and sports is not None:
1369
+ raise ValueError("Cannot specify both 'sport' and 'sports'.")
1371
1370
  if sport is not None:
1371
+ warnings.warn(
1372
+ "'sport' is deprecated, use 'sports' instead",
1373
+ DeprecationWarning,
1374
+ stacklevel=2,
1375
+ )
1372
1376
  sports = [sport]
1373
- elif sports is None:
1374
- sports = []
1377
+ resolved = sports if sports is not None else []
1375
1378
 
1376
1379
  params = {
1377
- "sports": self._enums_to_strings(sports),
1380
+ "sport": self._enums_to_strings(resolved),
1378
1381
  "start": start
1379
1382
  }
1380
1383
  if end is not None:
@@ -1407,42 +1410,64 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1407
1410
  def get_longitudinal_mean_max(
1408
1411
  self,
1409
1412
  *,
1410
- sport: Sport | str,
1413
+ sports: list[Sport | str] | None = None,
1414
+ sport: Sport | str | None = None,
1411
1415
  metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
1416
+ start: date | str | None = None,
1417
+ end: date | str | None = None,
1412
1418
  date: date | str | None = None,
1413
1419
  window_days: int | None = None,
1414
1420
  ) -> pd.DataFrame:
1415
- """Gets the mean-max curve for a specific sport and metric.
1416
-
1417
- This method retrieves the mean-max curve data for a given sport and metric,
1418
- optionally filtered by date and window size.
1421
+ """Gets the mean-max curve for one or more sports and a metric.
1419
1422
 
1420
1423
  Args:
1421
- sport: The sport to get mean-max data for. Can be a Sport enum or string ID.
1424
+ sports: List of sports to get mean-max data for. Can be Sport enums or strings.
1425
+ sport: Deprecated. Use ``sports`` instead.
1422
1426
  metric: The metric to calculate mean-max for. Must be either "power" or "speed".
1423
- date: Optional reference date for the mean-max calculation. If provided,
1424
- the mean-max curve will be calculated up to this date. Can be a date object
1425
- or string in ISO format.
1426
- window_days: Optional number of days to include in the calculation window
1427
- before the reference date. If None, all available data is used.
1427
+ start: Start of the date range. Preferred over ``date``/``window_days``.
1428
+ end: End of the date range (defaults to today). Used with ``start``.
1429
+ date: Deprecated. Use ``start`` and ``end`` instead.
1430
+ window_days: Deprecated. Use ``start`` and ``end`` instead.
1428
1431
 
1429
1432
  Returns:
1430
1433
  pd.DataFrame: A pandas DataFrame containing the mean-max curve data.
1431
1434
 
1432
1435
  Raises:
1436
+ ValueError: If both ``sport`` and ``sports`` are provided, or neither is provided.
1433
1437
  HTTPStatusError: If the API request fails.
1434
1438
  """
1435
- sport = self._enums_to_strings([sport])[0]
1439
+ if sport is not None and sports is not None:
1440
+ raise ValueError("Cannot specify both 'sport' and 'sports'.")
1441
+ if sport is not None:
1442
+ warnings.warn(
1443
+ "'sport' is deprecated, use 'sports' instead",
1444
+ DeprecationWarning,
1445
+ stacklevel=2,
1446
+ )
1447
+ sports = [sport]
1448
+ if sports is None:
1449
+ raise ValueError("'sports' is required.")
1436
1450
  metric = self._enums_to_strings([metric])[0]
1437
1451
 
1438
1452
  params = {
1439
- "sport": sport,
1453
+ "sport": self._enums_to_strings(sports),
1440
1454
  "metric": metric,
1441
1455
  }
1442
- if date is not None:
1443
- params["date"] = date
1444
- if window_days is not None:
1445
- params["window_days"] = window_days
1456
+ if start is not None:
1457
+ params["start"] = start
1458
+ if end is not None:
1459
+ params["end"] = end
1460
+ else:
1461
+ if date is not None or window_days is not None:
1462
+ warnings.warn(
1463
+ "'date' and 'window_days' are deprecated, use 'start' and 'end' instead",
1464
+ DeprecationWarning,
1465
+ stacklevel=2,
1466
+ )
1467
+ if date is not None:
1468
+ params["date"] = date
1469
+ if window_days is not None:
1470
+ params["window_days"] = window_days
1446
1471
 
1447
1472
  with self._http_client() as client:
1448
1473
  response = client.get(
@@ -1457,12 +1482,15 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1457
1482
  def get_longitudinal_awd(
1458
1483
  self,
1459
1484
  *,
1460
- sport: Sport | str,
1485
+ sports: list[Sport | str] | None = None,
1486
+ sport: Sport | str | None = None,
1461
1487
  metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
1488
+ start: date | str | None = None,
1489
+ end: date | str | None = None,
1462
1490
  date: date | str | None = None,
1463
1491
  window_days: int | None = None,
1464
1492
  ) -> pd.DataFrame:
1465
- """Gets the longitudinal accumulated work duration (AWD) for a specific sport and metric.
1493
+ """Gets the longitudinal accumulated work duration (AWD) for one or more sports.
1466
1494
 
1467
1495
  This method retrieves AWD values across four intensity levels: max (highest daily AWD),
1468
1496
  hard, medium, and easy (sustainable durations for respective workout intensities).
@@ -1470,31 +1498,53 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1470
1498
  Note: This endpoint is in development and subject to change.
1471
1499
 
1472
1500
  Args:
1473
- sport: The sport to get AWD data for. Can be a Sport enum or string ID.
1501
+ sports: List of sports to get AWD data for. Can be Sport enums or strings.
1502
+ sport: Deprecated. Use ``sports`` instead.
1474
1503
  metric: The metric to calculate AWD for. Must be either "power" or "speed".
1475
- date: Optional reference date for the AWD calculation. If provided,
1476
- the AWD will be calculated up to this date. Can be a date object
1477
- or string in ISO format.
1478
- window_days: Optional number of days to include in the calculation window
1479
- before the reference date. If None, all available data is used.
1504
+ start: Start of the date range. Preferred over ``date``/``window_days``.
1505
+ end: End of the date range (defaults to today). Used with ``start``.
1506
+ date: Deprecated. Use ``start`` and ``end`` instead.
1507
+ window_days: Deprecated. Use ``start`` and ``end`` instead.
1480
1508
 
1481
1509
  Returns:
1482
1510
  pd.DataFrame: A pandas DataFrame containing the longitudinal AWD data with intensity levels.
1483
1511
 
1484
1512
  Raises:
1513
+ ValueError: If both ``sport`` and ``sports`` are provided, or neither is provided.
1485
1514
  HTTPStatusError: If the API request fails.
1486
1515
  """
1487
- sport = self._enums_to_strings([sport])[0]
1516
+ if sport is not None and sports is not None:
1517
+ raise ValueError("Cannot specify both 'sport' and 'sports'.")
1518
+ if sport is not None:
1519
+ warnings.warn(
1520
+ "'sport' is deprecated, use 'sports' instead",
1521
+ DeprecationWarning,
1522
+ stacklevel=2,
1523
+ )
1524
+ sports = [sport]
1525
+ if sports is None:
1526
+ raise ValueError("'sports' is required.")
1488
1527
  metric = self._enums_to_strings([metric])[0]
1489
1528
 
1490
1529
  params = {
1491
- "sport": sport,
1530
+ "sport": self._enums_to_strings(sports),
1492
1531
  "metric": metric,
1493
1532
  }
1494
- if date is not None:
1495
- params["date"] = date
1496
- if window_days is not None:
1497
- params["window_days"] = window_days
1533
+ if start is not None:
1534
+ params["start"] = start
1535
+ if end is not None:
1536
+ params["end"] = end
1537
+ else:
1538
+ if date is not None or window_days is not None:
1539
+ warnings.warn(
1540
+ "'date' and 'window_days' are deprecated, use 'start' and 'end' instead",
1541
+ DeprecationWarning,
1542
+ stacklevel=2,
1543
+ )
1544
+ if date is not None:
1545
+ params["date"] = date
1546
+ if window_days is not None:
1547
+ params["window_days"] = window_days
1498
1548
 
1499
1549
  with self._http_client() as client:
1500
1550
  response = client.get(
@@ -1526,7 +1576,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1526
1576
  if end is not None:
1527
1577
  params["end"] = end.isoformat()
1528
1578
  if sports is not None:
1529
- params["sports"] = self._enums_to_strings(sports)
1579
+ params["sport"] = self._enums_to_strings(sports)
1530
1580
  if tags is not None:
1531
1581
  params["tags"] = tags
1532
1582
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes