sweatstack 0.89.0__tar.gz → 0.90.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.
- {sweatstack-0.89.0 → sweatstack-0.90.0}/CHANGELOG.md +23 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/PKG-INFO +1 -1
- {sweatstack-0.89.0 → sweatstack-0.90.0}/pyproject.toml +1 -1
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/client.py +50 -56
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_longitudinal_mean_max_after.py +13 -20
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_segmentation.py +15 -3
- {sweatstack-0.89.0 → sweatstack-0.90.0}/uv.lock +1 -1
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.claude/settings.local.json +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.claude/skills/sweatstack-python/client.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.github/workflows/ci.yml +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.gitignore +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/.python-version +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/AGENTS.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/LICENSE +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/Makefile +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/README.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/docs/conf.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/docs/everything.rst +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/docs/index.rst +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/002_TYPED_EXCEPTIONS.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/003_trace_test_linking.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/004_codebase_hygiene.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/005_ost_sport_bridge.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/006_output_backends.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/plans/007_account_status_userinfo_portal.md +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/_frames.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/exceptions.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/access_token_cache.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/__init__.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_access_token_cache.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_account_status.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_exceptions.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_frames.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_identity.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_output.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_portal.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_public_surface.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_sport_ost.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_teams.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_tests.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_trace_test_linking.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_webhooks.py +0 -0
- {sweatstack-0.89.0 → sweatstack-0.90.0}/tests/test_write_timezone_validation.py +0 -0
|
@@ -6,6 +6,29 @@ 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.90.0] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **BREAKING: mean-max curves are one row per duration** (server plan 063).
|
|
14
|
+
`get_activity_mean_max`, `get_latest_activity_mean_max` and `get_longitudinal_mean_max` return
|
|
15
|
+
`duration`, the metric (W or m/s) and `start` (UTC timestamp of the best effort), plus
|
|
16
|
+
`activity_id`, `sport` and `after` on the longitudinal curve. By default 19 durations from 1 s to
|
|
17
|
+
6 h; the curve can rise again at longer durations and is returned as it is.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `durations=` on all three mean-max methods: `None` for the 19 defaults, `"all"` for the full grid,
|
|
22
|
+
or a list of seconds. Keyword-only.
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- **BREAKING:** `segmentation` on `get_activity_mean_max` and `get_latest_activity_mean_max` (it never
|
|
27
|
+
reduced the payload; the server ignores it). It was positional: passing `True` in that slot now
|
|
28
|
+
raises `TypeError`.
|
|
29
|
+
- **BREAKING:** `by` on `get_longitudinal_mean_max`, and its `DeprecationWarning`. Every curve is
|
|
30
|
+
duration-oriented.
|
|
31
|
+
|
|
9
32
|
## [0.89.0] - 2026-09-28
|
|
10
33
|
|
|
11
34
|
Frames on your terms. Every method that returns a collection takes `output=`:
|
|
@@ -20,6 +20,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
20
20
|
from importlib.metadata import version
|
|
21
21
|
from inspect import getmembers, isfunction
|
|
22
22
|
from pathlib import Path
|
|
23
|
+
from collections.abc import Sequence
|
|
23
24
|
from typing import TYPE_CHECKING, Any, Dict, Generator, Literal, overload
|
|
24
25
|
|
|
25
26
|
from pydantic import SecretStr
|
|
@@ -715,6 +716,15 @@ class _DelegationMixin:
|
|
|
715
716
|
)
|
|
716
717
|
|
|
717
718
|
|
|
719
|
+
def _with_durations(params: dict, durations: "Sequence[int] | str | None") -> dict:
|
|
720
|
+
"""Add the mean-max ``durations`` query value: omitted, ``"all"``, or comma-separated seconds."""
|
|
721
|
+
if durations is None:
|
|
722
|
+
return params
|
|
723
|
+
if isinstance(durations, str):
|
|
724
|
+
return {**params, "durations": durations}
|
|
725
|
+
return {**params, "durations": ",".join(str(int(d)) for d in durations)}
|
|
726
|
+
|
|
727
|
+
|
|
718
728
|
class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixin):
|
|
719
729
|
"""SweatStack API client for accessing activities, traces, and user data.
|
|
720
730
|
|
|
@@ -1393,8 +1403,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1393
1403
|
self,
|
|
1394
1404
|
activity_id: str,
|
|
1395
1405
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1396
|
-
segmentation: bool = False,
|
|
1397
1406
|
*,
|
|
1407
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1398
1408
|
output: None = None,
|
|
1399
1409
|
) -> pd.DataFrame | pl.DataFrame: ...
|
|
1400
1410
|
|
|
@@ -1403,8 +1413,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1403
1413
|
self,
|
|
1404
1414
|
activity_id: str,
|
|
1405
1415
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1406
|
-
segmentation: bool = False,
|
|
1407
1416
|
*,
|
|
1417
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1408
1418
|
output: Literal["pandas"],
|
|
1409
1419
|
) -> pd.DataFrame: ...
|
|
1410
1420
|
|
|
@@ -1413,8 +1423,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1413
1423
|
self,
|
|
1414
1424
|
activity_id: str,
|
|
1415
1425
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1416
|
-
segmentation: bool = False,
|
|
1417
1426
|
*,
|
|
1427
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1418
1428
|
output: Literal["polars"],
|
|
1419
1429
|
) -> pl.DataFrame: ...
|
|
1420
1430
|
|
|
@@ -1423,8 +1433,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1423
1433
|
self,
|
|
1424
1434
|
activity_id: str,
|
|
1425
1435
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1426
|
-
segmentation: bool = False,
|
|
1427
1436
|
*,
|
|
1437
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1428
1438
|
output: Literal["arrow"],
|
|
1429
1439
|
) -> pa.Table: ...
|
|
1430
1440
|
|
|
@@ -1433,8 +1443,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1433
1443
|
self,
|
|
1434
1444
|
activity_id: str,
|
|
1435
1445
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1436
|
-
segmentation: bool = False,
|
|
1437
1446
|
*,
|
|
1447
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1438
1448
|
output: Literal["bytes"],
|
|
1439
1449
|
) -> bytes: ...
|
|
1440
1450
|
|
|
@@ -1442,8 +1452,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1442
1452
|
self,
|
|
1443
1453
|
activity_id: str,
|
|
1444
1454
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1445
|
-
segmentation: bool = False,
|
|
1446
1455
|
*,
|
|
1456
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1447
1457
|
output: FrameOutput | None = None,
|
|
1448
1458
|
) -> pd.DataFrame | pl.DataFrame | pa.Table | bytes:
|
|
1449
1459
|
"""Gets the mean-max data for a specific activity.
|
|
@@ -1454,14 +1464,17 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1454
1464
|
Args:
|
|
1455
1465
|
activity_id: The unique identifier of the activity.
|
|
1456
1466
|
metric: The metric to calculate mean-max values for, either "power" or "speed".
|
|
1457
|
-
|
|
1458
|
-
for
|
|
1467
|
+
durations: The durations to return, in seconds. ``None`` (default) for 19 durations from
|
|
1468
|
+
1 s to 6 h, ``"all"`` for the full grid (1 s steps to 3 min, then 5, 10, 30 and 60 s
|
|
1469
|
+
steps), or a list of seconds. Durations the activity did not last are left out.
|
|
1459
1470
|
output: ``"pandas"``, ``"polars"``, ``"arrow"`` or ``"bytes"`` (the raw parquet
|
|
1460
1471
|
response). Defaults to the installed frame library, Polars if both are.
|
|
1461
1472
|
Overrides the client-level default for this call.
|
|
1462
1473
|
|
|
1463
1474
|
Returns:
|
|
1464
|
-
A frame (per ``output``)
|
|
1475
|
+
A frame (per ``output``), one row per duration: ``duration``, the metric (power in W or
|
|
1476
|
+
speed in m/s) and ``start`` (UTC timestamp at which that best effort began). The curve
|
|
1477
|
+
is returned as it is and can rise again at longer durations (intermittent efforts).
|
|
1465
1478
|
|
|
1466
1479
|
Raises:
|
|
1467
1480
|
SweatStackNotFoundError: If the activity does not exist.
|
|
@@ -1471,10 +1484,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1471
1484
|
with self._http_client() as client:
|
|
1472
1485
|
response = client.get(
|
|
1473
1486
|
url=f"/api/v1/activities/{activity_id}/mean-max",
|
|
1474
|
-
params={
|
|
1475
|
-
"metric": metric,
|
|
1476
|
-
"segmentation": segmentation,
|
|
1477
|
-
},
|
|
1487
|
+
params=_with_durations({"metric": metric}, durations),
|
|
1478
1488
|
)
|
|
1479
1489
|
self._raise_for_status(response)
|
|
1480
1490
|
return self._read_frame(response.content, output)
|
|
@@ -1650,8 +1660,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1650
1660
|
self,
|
|
1651
1661
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1652
1662
|
sport: Sport | str | None = None,
|
|
1653
|
-
segmentation: bool = False,
|
|
1654
1663
|
*,
|
|
1664
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1655
1665
|
output: None = None,
|
|
1656
1666
|
) -> pd.DataFrame | pl.DataFrame: ...
|
|
1657
1667
|
|
|
@@ -1660,8 +1670,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1660
1670
|
self,
|
|
1661
1671
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1662
1672
|
sport: Sport | str | None = None,
|
|
1663
|
-
segmentation: bool = False,
|
|
1664
1673
|
*,
|
|
1674
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1665
1675
|
output: Literal["pandas"],
|
|
1666
1676
|
) -> pd.DataFrame: ...
|
|
1667
1677
|
|
|
@@ -1670,8 +1680,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1670
1680
|
self,
|
|
1671
1681
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1672
1682
|
sport: Sport | str | None = None,
|
|
1673
|
-
segmentation: bool = False,
|
|
1674
1683
|
*,
|
|
1684
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1675
1685
|
output: Literal["polars"],
|
|
1676
1686
|
) -> pl.DataFrame: ...
|
|
1677
1687
|
|
|
@@ -1680,8 +1690,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1680
1690
|
self,
|
|
1681
1691
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1682
1692
|
sport: Sport | str | None = None,
|
|
1683
|
-
segmentation: bool = False,
|
|
1684
1693
|
*,
|
|
1694
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1685
1695
|
output: Literal["arrow"],
|
|
1686
1696
|
) -> pa.Table: ...
|
|
1687
1697
|
|
|
@@ -1690,8 +1700,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1690
1700
|
self,
|
|
1691
1701
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1692
1702
|
sport: Sport | str | None = None,
|
|
1693
|
-
segmentation: bool = False,
|
|
1694
1703
|
*,
|
|
1704
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1695
1705
|
output: Literal["bytes"],
|
|
1696
1706
|
) -> bytes: ...
|
|
1697
1707
|
|
|
@@ -1699,8 +1709,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1699
1709
|
self,
|
|
1700
1710
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1701
1711
|
sport: Sport | str | None = None,
|
|
1702
|
-
segmentation: bool = False,
|
|
1703
1712
|
*,
|
|
1713
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1704
1714
|
output: FrameOutput | None = None,
|
|
1705
1715
|
) -> pd.DataFrame | pl.DataFrame | pa.Table | bytes:
|
|
1706
1716
|
"""Gets the mean-max curve for the latest activity of a specific sport.
|
|
@@ -1711,20 +1721,21 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1711
1721
|
Args:
|
|
1712
1722
|
metric: The metric to calculate the mean-max curve for. Can be either "power" or "speed".
|
|
1713
1723
|
sport: Optional sport to filter by. Can be a Sport enum or string.
|
|
1714
|
-
|
|
1715
|
-
Defaults to False.
|
|
1724
|
+
durations: As for :meth:`get_activity_mean_max`.
|
|
1716
1725
|
output: ``"pandas"``, ``"polars"``, ``"arrow"`` or ``"bytes"`` (the raw parquet
|
|
1717
1726
|
response). Defaults to the installed frame library, Polars if both are.
|
|
1718
1727
|
Overrides the client-level default for this call.
|
|
1719
1728
|
|
|
1720
1729
|
Returns:
|
|
1721
|
-
A frame (per ``output``)
|
|
1730
|
+
A frame (per ``output``), one row per duration: ``duration``, the metric (power in W or
|
|
1731
|
+
speed in m/s) and ``start`` (UTC timestamp at which that best effort began). The curve
|
|
1732
|
+
is returned as it is and can rise again at longer durations (intermittent efforts).
|
|
1722
1733
|
|
|
1723
1734
|
Raises:
|
|
1724
1735
|
SweatStackAPIError: If the API request fails.
|
|
1725
1736
|
"""
|
|
1726
1737
|
activity = self.get_latest_activity(sport=sport)
|
|
1727
|
-
return self.get_activity_mean_max(activity.id, metric,
|
|
1738
|
+
return self.get_activity_mean_max(activity.id, metric, durations=durations, output=output)
|
|
1728
1739
|
|
|
1729
1740
|
@overload
|
|
1730
1741
|
def get_longitudinal_data(
|
|
@@ -1879,7 +1890,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1879
1890
|
date: date | str | None = None,
|
|
1880
1891
|
window_days: int | None = None,
|
|
1881
1892
|
after: list[float] | float | None = None,
|
|
1882
|
-
|
|
1893
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1883
1894
|
output: None = None,
|
|
1884
1895
|
) -> pd.DataFrame | pl.DataFrame: ...
|
|
1885
1896
|
|
|
@@ -1895,7 +1906,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1895
1906
|
date: date | str | None = None,
|
|
1896
1907
|
window_days: int | None = None,
|
|
1897
1908
|
after: list[float] | float | None = None,
|
|
1898
|
-
|
|
1909
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1899
1910
|
output: Literal["pandas"],
|
|
1900
1911
|
) -> pd.DataFrame: ...
|
|
1901
1912
|
|
|
@@ -1911,7 +1922,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1911
1922
|
date: date | str | None = None,
|
|
1912
1923
|
window_days: int | None = None,
|
|
1913
1924
|
after: list[float] | float | None = None,
|
|
1914
|
-
|
|
1925
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1915
1926
|
output: Literal["polars"],
|
|
1916
1927
|
) -> pl.DataFrame: ...
|
|
1917
1928
|
|
|
@@ -1927,7 +1938,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1927
1938
|
date: date | str | None = None,
|
|
1928
1939
|
window_days: int | None = None,
|
|
1929
1940
|
after: list[float] | float | None = None,
|
|
1930
|
-
|
|
1941
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1931
1942
|
output: Literal["arrow"],
|
|
1932
1943
|
) -> pa.Table: ...
|
|
1933
1944
|
|
|
@@ -1943,7 +1954,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1943
1954
|
date: date | str | None = None,
|
|
1944
1955
|
window_days: int | None = None,
|
|
1945
1956
|
after: list[float] | float | None = None,
|
|
1946
|
-
|
|
1957
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1947
1958
|
output: Literal["bytes"],
|
|
1948
1959
|
) -> bytes: ...
|
|
1949
1960
|
|
|
@@ -1958,7 +1969,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1958
1969
|
date: date | str | None = None,
|
|
1959
1970
|
window_days: int | None = None,
|
|
1960
1971
|
after: list[float] | float | None = None,
|
|
1961
|
-
|
|
1972
|
+
durations: Sequence[int] | Literal["all"] | None = None,
|
|
1962
1973
|
output: FrameOutput | None = None,
|
|
1963
1974
|
) -> pd.DataFrame | pl.DataFrame | pa.Table | bytes:
|
|
1964
1975
|
"""Gets the mean-max curve for one or more sports and a metric.
|
|
@@ -1977,22 +1988,19 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1977
1988
|
enveloped across rides. The returned DataFrame then has an ``after``
|
|
1978
1989
|
column (one curve per value). Max 5 values; the date range is capped at
|
|
1979
1990
|
1 year when ``after`` is used.
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
``after`` case ``"duration"`` will become the only supported orientation, and
|
|
1984
|
-
more generally ``"duration"`` is set to become the default and only option, so
|
|
1985
|
-
we recommend passing ``by="duration"`` explicitly. When left as ``None``
|
|
1986
|
-
(default), the server picks: ``after`` with ``metric="power"`` resolves to
|
|
1987
|
-
``"duration"``, every other case to ``"intensity"``.
|
|
1991
|
+
durations: The durations to return, in seconds. ``None`` (default) for 19 durations from
|
|
1992
|
+
1 s to 6 h, ``"all"`` for the full grid (1 s steps to 3 min, then 5, 10, 30 and 60 s
|
|
1993
|
+
steps), or a list of seconds. Durations no activity lasted are left out.
|
|
1988
1994
|
output: ``"pandas"``, ``"polars"``, ``"arrow"`` or ``"bytes"`` (the raw parquet
|
|
1989
1995
|
response). Defaults to the installed frame library, Polars if both are.
|
|
1990
1996
|
Overrides the client-level default for this call.
|
|
1991
1997
|
|
|
1992
1998
|
Returns:
|
|
1993
|
-
A frame (per ``output``)
|
|
1994
|
-
|
|
1995
|
-
|
|
1999
|
+
A frame (per ``output``), one row per duration: ``duration``, the metric (power in W or
|
|
2000
|
+
speed in m/s), ``start`` (UTC timestamp at which that best effort began), and the
|
|
2001
|
+
``activity_id`` and ``sport`` that set it. With ``after``, an ``after`` column
|
|
2002
|
+
distinguishes the fatigue states. The curve is returned as it is and can rise again at
|
|
2003
|
+
longer durations (intermittent efforts).
|
|
1996
2004
|
|
|
1997
2005
|
Raises:
|
|
1998
2006
|
ValueError: If both ``sport`` and ``sports`` are provided, or neither is provided.
|
|
@@ -2011,24 +2019,10 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
2011
2019
|
raise ValueError("'sports' is required.")
|
|
2012
2020
|
metric = self._enums_to_strings([metric])[0]
|
|
2013
2021
|
|
|
2014
|
-
|
|
2015
|
-
# orientation going forward; by='intensity' is deprecated there. (For metric='speed'
|
|
2016
|
-
# it is still the only option, so no warning.)
|
|
2017
|
-
if by == "intensity" and after is not None and metric == "power":
|
|
2018
|
-
warnings.warn(
|
|
2019
|
-
"by='intensity' is deprecated for 'after' (fatigue) mean-max; by='duration' "
|
|
2020
|
-
"is the default and only supported orientation going forward. Pass "
|
|
2021
|
-
"by='duration' or leave 'by' unset.",
|
|
2022
|
-
DeprecationWarning,
|
|
2023
|
-
stacklevel=2,
|
|
2024
|
-
)
|
|
2025
|
-
|
|
2026
|
-
params = {
|
|
2022
|
+
params = _with_durations({
|
|
2027
2023
|
"sport": self._enums_to_strings(sports),
|
|
2028
2024
|
"metric": metric,
|
|
2029
|
-
}
|
|
2030
|
-
if by is not None:
|
|
2031
|
-
params["by"] = by
|
|
2025
|
+
}, durations)
|
|
2032
2026
|
if start is not None:
|
|
2033
2027
|
params["start"] = start
|
|
2034
2028
|
if end is not None:
|
|
@@ -83,7 +83,7 @@ def test_no_after_is_unchanged(client):
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def _index_free_by_duration_response() -> bytes:
|
|
86
|
-
"""
|
|
86
|
+
"""An `after` response as the server returns it (plan 063): one row per duration, columns only."""
|
|
87
87
|
df = pd.DataFrame({
|
|
88
88
|
"duration": pd.to_timedelta([5, 60, 300, 5, 60], unit="s"),
|
|
89
89
|
"power": [400.0, 300.0, 250.0, 380.0, 290.0],
|
|
@@ -96,25 +96,22 @@ def _index_free_by_duration_response() -> bytes:
|
|
|
96
96
|
return buf.getvalue()
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def
|
|
100
|
-
# `by` unset -> omitted from the request so the server picks the orientation (0.85.0+),
|
|
101
|
-
# rather than the client forcing the deprecated value-indexed path.
|
|
99
|
+
def test_durations_are_omitted_by_default_and_sent_when_given(client):
|
|
102
100
|
_result, params = _call(client, after=[0, 500])
|
|
103
|
-
assert "by" not in params
|
|
101
|
+
assert "durations" not in params and "by" not in params
|
|
102
|
+
_result, params = _call(client, after=[0, 500], durations=[5, 60, 300])
|
|
103
|
+
assert params["durations"] == "5,60,300"
|
|
104
|
+
_result, params = _call(client, durations="all")
|
|
105
|
+
assert params["durations"] == "all"
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
def
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
result, params = _call(client, after=[0, 500], by="intensity")
|
|
111
|
-
assert params["by"] == "intensity"
|
|
112
|
-
assert isinstance(result.index, pd.RangeIndex)
|
|
113
|
-
assert {"power", "after"} <= set(result.columns)
|
|
114
|
-
assert result["duration"].dtype == "timedelta64[ns]"
|
|
108
|
+
def test_by_is_removed(client):
|
|
109
|
+
# Server plan 063: every mean-max response is duration-oriented; `by` is gone.
|
|
110
|
+
with pytest.raises(TypeError):
|
|
111
|
+
_call(client, after=[0, 50], by="duration")
|
|
115
112
|
|
|
116
113
|
|
|
117
|
-
def
|
|
114
|
+
def test_after_response_is_duration_rows_with_columns_only(client):
|
|
118
115
|
response = MagicMock(status_code=200, content=_index_free_by_duration_response())
|
|
119
116
|
http = MagicMock()
|
|
120
117
|
http.__enter__ = MagicMock(return_value=http)
|
|
@@ -123,11 +120,7 @@ def test_by_duration_is_sent_and_duration_indexed(client):
|
|
|
123
120
|
with patch.object(client, "_http_client", return_value=http), \
|
|
124
121
|
patch.object(client, "_raise_for_status"), \
|
|
125
122
|
patch.object(client, "_cache_enabled", return_value=False):
|
|
126
|
-
result = client.get_longitudinal_mean_max(
|
|
127
|
-
sports=["cycling"], metric="power", after=[0, 50], by="duration", output="pandas")
|
|
128
|
-
params = http.get.call_args.kwargs["params"]
|
|
129
|
-
assert params["by"] == "duration"
|
|
130
|
-
# duration, power and after are all columns; nothing is indexed
|
|
123
|
+
result = client.get_longitudinal_mean_max(sports=["cycling"], metric="power", after=[0, 50], output="pandas")
|
|
131
124
|
assert isinstance(result.index, pd.RangeIndex)
|
|
132
125
|
assert {"duration", "power", "after"} <= set(result.columns)
|
|
133
126
|
assert set(result["after"].unique()) == {0.0, 50.0}
|
|
@@ -56,9 +56,21 @@ def test_get_longitudinal_data_sends_segmentation_on(client):
|
|
|
56
56
|
assert params["segmentation_on"] == "power"
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
def test_get_activity_mean_max_sends_durations(client):
|
|
60
|
+
assert "durations" not in _params_sent(client, lambda: client.get_activity_mean_max("a", "power"))
|
|
61
|
+
params = _params_sent(client, lambda: client.get_activity_mean_max("a", "power", durations=[300, 5]))
|
|
62
|
+
assert params["durations"] == "300,5"
|
|
63
|
+
params = _params_sent(client, lambda: client.get_activity_mean_max("a", "power", durations="all"))
|
|
64
|
+
assert params["durations"] == "all"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_mean_max_segmentation_is_removed(client):
|
|
68
|
+
# Server plan 063: `segmentation` never reduced the payload and is gone. A positional True
|
|
69
|
+
# in its old slot must fail loudly, not be read as durations.
|
|
70
|
+
with pytest.raises(TypeError):
|
|
71
|
+
client.get_activity_mean_max("a", "power", True)
|
|
72
|
+
with pytest.raises(TypeError):
|
|
73
|
+
client.get_activity_mean_max("a", "power", segmentation=True)
|
|
62
74
|
|
|
63
75
|
|
|
64
76
|
def test_old_nlec_kwargs_are_rejected(client):
|
|
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
|
|
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
|
|
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
|
{sweatstack-0.89.0 → sweatstack-0.90.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|