sweatstack 0.69.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.
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/client.md +19 -7
- {sweatstack-0.69.0 → sweatstack-0.71.0}/CHANGELOG.md +23 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/PKG-INFO +1 -1
- {sweatstack-0.69.0 → sweatstack-0.71.0}/pyproject.toml +1 -1
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/client.py +156 -76
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.claude/settings.local.json +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.gitignore +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/.python-version +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/CLIENT_DTYPE_CONVERSION.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/CLIENT_LIBRARY_SKILL.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/FASTAPI_DOCS.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/FASTAPI_PLUGIN.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/FASTAPI_USER_SWITCHING.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/FASTAPI_WEBHOOKS.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/LOCAL_AUTH.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/Makefile +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/README.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/docs/conf.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/docs/everything.rst +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/docs/index.rst +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/examples/tokens.db +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/fastapi_coaching_example.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/fastapi_example.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/fastapi_sweatstack.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/playground/README.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/playground/Untitled.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/playground/hello.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/playground/pyproject.toml +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/tests/__init__.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.71.0}/tests/test_webhooks.py +0 -0
- {sweatstack-0.69.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
|
-
|
|
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
|
-
#
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
@@ -176,6 +184,7 @@ user = client.create_user(first_name="John", last_name="Doe")
|
|
|
176
184
|
|
|
177
185
|
# Team management
|
|
178
186
|
team_users = client.get_team_users(team_id="team_abc")
|
|
187
|
+
athlete = client.get_team_user(team_id="team_abc", user="john") # by name or ID
|
|
179
188
|
client.authorize_team(team_id="team_abc", scopes=[Scope.data_read])
|
|
180
189
|
```
|
|
181
190
|
|
|
@@ -187,9 +196,12 @@ Operate on behalf of another user (requires appropriate permissions).
|
|
|
187
196
|
|
|
188
197
|
```python
|
|
189
198
|
other = client.delegated_client("john")
|
|
190
|
-
other = client.delegated_client("john", team_id="team_abc") # via team
|
|
191
199
|
other_activities = other.get_activities()
|
|
192
200
|
# original client is unchanged
|
|
201
|
+
|
|
202
|
+
# Via team membership
|
|
203
|
+
athlete = client.get_team_user(team_id="team_abc", user="john")
|
|
204
|
+
other = client.delegated_client(athlete, team_id="team_abc")
|
|
193
205
|
```
|
|
194
206
|
|
|
195
207
|
`switch_user()` modifies the client in-place — useful in interactive/notebook contexts but avoid in scripts:
|
|
@@ -248,7 +260,7 @@ df = sweatstack.get_latest_activity_data()
|
|
|
248
260
|
|
|
249
261
|
- **Sport enum uses underscores:** `Sport.cycling_road`, not `Sport("road")` or `Sport.cycling.road`. String values use dots: `"cycling.road"`.
|
|
250
262
|
- **`start` is required for longitudinal endpoints.** Unlike `get_activities()` where all filters are optional.
|
|
251
|
-
- **`sport` (singular) vs `sports` (list):** `get_latest_activity(sport=...)`
|
|
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.
|
|
252
264
|
- **DataFrames have standard dtypes.** The library converts API-optimized types (Int16, float16) to float64/datetime64[ns] automatically.
|
|
253
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.
|
|
254
266
|
- **`summary` fields are optional.** Always null-check: `activity.summary.power.mean if activity.summary and activity.summary.power else None`.
|
|
@@ -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.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
|
+
|
|
23
|
+
## [0.70.0] - 2026-03-13
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Added `get_team_user(*, team_id, user, search_mode)` method to find a single team-authorized user by ID or name.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Refactored internal user lookup into reusable `_find_user()`, `_find_user_by_name()`, and `_find_user_by_id()` helpers shared by `get_user()` and `get_team_user()`.
|
|
30
|
+
|
|
31
|
+
|
|
9
32
|
## [0.69.0] - 2026-03-12
|
|
10
33
|
|
|
11
34
|
### Added
|
|
@@ -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
|
|
@@ -498,72 +499,77 @@ class _DelegationMixin:
|
|
|
498
499
|
|
|
499
500
|
return len(user) in (16, 26) and user.isalnum()
|
|
500
501
|
|
|
501
|
-
def
|
|
502
|
-
"""
|
|
502
|
+
def _find_user_by_name(self, name: str, users: list) -> UserSummary:
|
|
503
|
+
"""Find a user by name from a list of users.
|
|
503
504
|
|
|
504
505
|
Args:
|
|
505
|
-
name: The
|
|
506
|
+
name: The (partial) display name to search for.
|
|
507
|
+
users: The list of UserSummary objects to search.
|
|
506
508
|
|
|
507
509
|
Returns:
|
|
508
|
-
UserSummary: The user
|
|
510
|
+
UserSummary: The matching user.
|
|
509
511
|
|
|
510
512
|
Raises:
|
|
511
|
-
ValueError: If
|
|
512
|
-
ValueError: If multiple users are found with the same name.
|
|
513
|
+
ValueError: If no match or multiple matches found.
|
|
513
514
|
"""
|
|
514
|
-
matches = []
|
|
515
|
-
for user in self.get_users():
|
|
516
|
-
if name in user.display_name.lower():
|
|
517
|
-
matches.append(user)
|
|
515
|
+
matches = [u for u in users if name in u.display_name.lower()]
|
|
518
516
|
|
|
519
517
|
if len(matches) == 0:
|
|
520
518
|
raise ValueError(f"User with name {name} not found")
|
|
521
519
|
elif len(matches) > 1:
|
|
522
|
-
raise ValueError(f"Multiple users found with name {name}: {', '.join([
|
|
520
|
+
raise ValueError(f"Multiple users found with name {name}: {', '.join([u.display_name for u in matches])}")
|
|
523
521
|
return matches[0]
|
|
524
522
|
|
|
525
|
-
def
|
|
526
|
-
"""
|
|
523
|
+
def _find_user_by_id(self, id: str, users: list) -> UserSummary:
|
|
524
|
+
"""Find a user by ID from a list of users.
|
|
527
525
|
|
|
528
526
|
Args:
|
|
529
|
-
id: The ID
|
|
527
|
+
id: The user ID to search for.
|
|
528
|
+
users: The list of UserSummary objects to search.
|
|
530
529
|
|
|
531
530
|
Returns:
|
|
532
|
-
UserSummary: The user
|
|
531
|
+
UserSummary: The matching user, or None if not found.
|
|
532
|
+
"""
|
|
533
|
+
return next((u for u in users if u.id == id), None)
|
|
533
534
|
|
|
534
|
-
|
|
535
|
-
|
|
535
|
+
def _find_user(self, user: str, users: list, search_mode: Literal["auto", "id", "name"] = "auto") -> UserSummary:
|
|
536
|
+
"""Find a user by ID or name from a list of users.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
user: User ID or (part of) display name.
|
|
540
|
+
users: The list of UserSummary objects to search.
|
|
541
|
+
search_mode: "auto" (detect), "id", or "name".
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
UserSummary: The matching user.
|
|
536
545
|
"""
|
|
537
|
-
|
|
538
|
-
|
|
546
|
+
if search_mode == "auto":
|
|
547
|
+
if self._is_user_id(user):
|
|
548
|
+
return self._find_user_by_id(user, users)
|
|
549
|
+
else:
|
|
550
|
+
return self._find_user_by_name(user, users)
|
|
551
|
+
elif search_mode == "id":
|
|
552
|
+
return self._find_user_by_id(user, users)
|
|
553
|
+
elif search_mode == "name":
|
|
554
|
+
return self._find_user_by_name(user, users)
|
|
539
555
|
|
|
540
556
|
def get_user(self, user: str, *, search_mode: Literal["auto", "id", "name"] = "auto") -> UserSummary:
|
|
541
557
|
"""Get a user by ID or name.
|
|
542
558
|
This method will always authenticate as the principal user.
|
|
543
559
|
|
|
544
560
|
Args:
|
|
545
|
-
user:
|
|
546
|
-
search_mode:
|
|
547
|
-
- "auto": Automatically determine the search mode based on the type of user argument.
|
|
548
|
-
- "id": Search for the user by ID.
|
|
549
|
-
- "name": Search for the user by name.
|
|
561
|
+
user: User ID or (part of) display name.
|
|
562
|
+
search_mode: "auto" (detect), "id", or "name".
|
|
550
563
|
|
|
551
564
|
Returns:
|
|
552
565
|
UserSummary: The user object.
|
|
553
566
|
|
|
554
567
|
Raises:
|
|
555
|
-
|
|
568
|
+
ValueError: If no match or multiple matches found.
|
|
556
569
|
"""
|
|
557
570
|
client = self.principal_client()
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return client._get_user_by_id(user)
|
|
561
|
-
else:
|
|
562
|
-
return client._get_user_by_name(user)
|
|
563
|
-
elif search_mode == "id":
|
|
564
|
-
return client._get_user_by_id(user)
|
|
565
|
-
elif search_mode == "name":
|
|
566
|
-
return client._get_user_by_name(user)
|
|
571
|
+
users = client.get_users()
|
|
572
|
+
return client._find_user(user, users, search_mode)
|
|
567
573
|
|
|
568
574
|
def switch_user(
|
|
569
575
|
self,
|
|
@@ -997,7 +1003,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
997
1003
|
if end is not None:
|
|
998
1004
|
params["end"] = end.isoformat()
|
|
999
1005
|
if sports is not None:
|
|
1000
|
-
params["
|
|
1006
|
+
params["sport"] = self._enums_to_strings(sports)
|
|
1001
1007
|
if tags is not None:
|
|
1002
1008
|
params["tags"] = tags
|
|
1003
1009
|
|
|
@@ -1331,8 +1337,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1331
1337
|
def get_longitudinal_data(
|
|
1332
1338
|
self,
|
|
1333
1339
|
*,
|
|
1334
|
-
sport: Sport | str | None = None,
|
|
1335
1340
|
sports: list[Sport | str] | None = None,
|
|
1341
|
+
sport: Sport | str | None = None,
|
|
1336
1342
|
start: date | str,
|
|
1337
1343
|
end: date | str | None = None,
|
|
1338
1344
|
metrics: list[Metric | str] | None = None,
|
|
@@ -1344,10 +1350,8 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1344
1350
|
including sport type and date range. The data is returned as a pandas DataFrame.
|
|
1345
1351
|
|
|
1346
1352
|
Args:
|
|
1347
|
-
sport: Optional single sport to filter by. Can be a Sport enum or string.
|
|
1348
|
-
Cannot be used together with 'sports'.
|
|
1349
1353
|
sports: Optional list of sports to filter by. Can be a list of Sport enums or strings.
|
|
1350
|
-
|
|
1354
|
+
sport: Deprecated. Use ``sports`` instead.
|
|
1351
1355
|
start: The start date for the data range. Can be a date object or string in ISO format.
|
|
1352
1356
|
end: Optional end date for the data range. Can be a date object or string in ISO format.
|
|
1353
1357
|
metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
|
|
@@ -1361,15 +1365,19 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1361
1365
|
ValueError: If both 'sport' and 'sports' parameters are provided.
|
|
1362
1366
|
HTTPStatusError: If the API request fails.
|
|
1363
1367
|
"""
|
|
1364
|
-
if sport and sports:
|
|
1365
|
-
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'.")
|
|
1366
1370
|
if sport is not None:
|
|
1371
|
+
warnings.warn(
|
|
1372
|
+
"'sport' is deprecated, use 'sports' instead",
|
|
1373
|
+
DeprecationWarning,
|
|
1374
|
+
stacklevel=2,
|
|
1375
|
+
)
|
|
1367
1376
|
sports = [sport]
|
|
1368
|
-
|
|
1369
|
-
sports = []
|
|
1377
|
+
resolved = sports if sports is not None else []
|
|
1370
1378
|
|
|
1371
1379
|
params = {
|
|
1372
|
-
"
|
|
1380
|
+
"sport": self._enums_to_strings(resolved),
|
|
1373
1381
|
"start": start
|
|
1374
1382
|
}
|
|
1375
1383
|
if end is not None:
|
|
@@ -1402,42 +1410,64 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1402
1410
|
def get_longitudinal_mean_max(
|
|
1403
1411
|
self,
|
|
1404
1412
|
*,
|
|
1405
|
-
|
|
1413
|
+
sports: list[Sport | str] | None = None,
|
|
1414
|
+
sport: Sport | str | None = None,
|
|
1406
1415
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1416
|
+
start: date | str | None = None,
|
|
1417
|
+
end: date | str | None = None,
|
|
1407
1418
|
date: date | str | None = None,
|
|
1408
1419
|
window_days: int | None = None,
|
|
1409
1420
|
) -> pd.DataFrame:
|
|
1410
|
-
"""Gets the mean-max curve for
|
|
1411
|
-
|
|
1412
|
-
This method retrieves the mean-max curve data for a given sport and metric,
|
|
1413
|
-
optionally filtered by date and window size.
|
|
1421
|
+
"""Gets the mean-max curve for one or more sports and a metric.
|
|
1414
1422
|
|
|
1415
1423
|
Args:
|
|
1416
|
-
|
|
1424
|
+
sports: List of sports to get mean-max data for. Can be Sport enums or strings.
|
|
1425
|
+
sport: Deprecated. Use ``sports`` instead.
|
|
1417
1426
|
metric: The metric to calculate mean-max for. Must be either "power" or "speed".
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
window_days:
|
|
1422
|
-
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.
|
|
1423
1431
|
|
|
1424
1432
|
Returns:
|
|
1425
1433
|
pd.DataFrame: A pandas DataFrame containing the mean-max curve data.
|
|
1426
1434
|
|
|
1427
1435
|
Raises:
|
|
1436
|
+
ValueError: If both ``sport`` and ``sports`` are provided, or neither is provided.
|
|
1428
1437
|
HTTPStatusError: If the API request fails.
|
|
1429
1438
|
"""
|
|
1430
|
-
sport
|
|
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.")
|
|
1431
1450
|
metric = self._enums_to_strings([metric])[0]
|
|
1432
1451
|
|
|
1433
1452
|
params = {
|
|
1434
|
-
"sport":
|
|
1453
|
+
"sport": self._enums_to_strings(sports),
|
|
1435
1454
|
"metric": metric,
|
|
1436
1455
|
}
|
|
1437
|
-
if
|
|
1438
|
-
params["
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
|
1441
1471
|
|
|
1442
1472
|
with self._http_client() as client:
|
|
1443
1473
|
response = client.get(
|
|
@@ -1452,12 +1482,15 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1452
1482
|
def get_longitudinal_awd(
|
|
1453
1483
|
self,
|
|
1454
1484
|
*,
|
|
1455
|
-
|
|
1485
|
+
sports: list[Sport | str] | None = None,
|
|
1486
|
+
sport: Sport | str | None = None,
|
|
1456
1487
|
metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
|
|
1488
|
+
start: date | str | None = None,
|
|
1489
|
+
end: date | str | None = None,
|
|
1457
1490
|
date: date | str | None = None,
|
|
1458
1491
|
window_days: int | None = None,
|
|
1459
1492
|
) -> pd.DataFrame:
|
|
1460
|
-
"""Gets the longitudinal accumulated work duration (AWD) for
|
|
1493
|
+
"""Gets the longitudinal accumulated work duration (AWD) for one or more sports.
|
|
1461
1494
|
|
|
1462
1495
|
This method retrieves AWD values across four intensity levels: max (highest daily AWD),
|
|
1463
1496
|
hard, medium, and easy (sustainable durations for respective workout intensities).
|
|
@@ -1465,31 +1498,53 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1465
1498
|
Note: This endpoint is in development and subject to change.
|
|
1466
1499
|
|
|
1467
1500
|
Args:
|
|
1468
|
-
|
|
1501
|
+
sports: List of sports to get AWD data for. Can be Sport enums or strings.
|
|
1502
|
+
sport: Deprecated. Use ``sports`` instead.
|
|
1469
1503
|
metric: The metric to calculate AWD for. Must be either "power" or "speed".
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
window_days:
|
|
1474
|
-
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.
|
|
1475
1508
|
|
|
1476
1509
|
Returns:
|
|
1477
1510
|
pd.DataFrame: A pandas DataFrame containing the longitudinal AWD data with intensity levels.
|
|
1478
1511
|
|
|
1479
1512
|
Raises:
|
|
1513
|
+
ValueError: If both ``sport`` and ``sports`` are provided, or neither is provided.
|
|
1480
1514
|
HTTPStatusError: If the API request fails.
|
|
1481
1515
|
"""
|
|
1482
|
-
sport
|
|
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.")
|
|
1483
1527
|
metric = self._enums_to_strings([metric])[0]
|
|
1484
1528
|
|
|
1485
1529
|
params = {
|
|
1486
|
-
"sport":
|
|
1530
|
+
"sport": self._enums_to_strings(sports),
|
|
1487
1531
|
"metric": metric,
|
|
1488
1532
|
}
|
|
1489
|
-
if
|
|
1490
|
-
params["
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
1493
1548
|
|
|
1494
1549
|
with self._http_client() as client:
|
|
1495
1550
|
response = client.get(
|
|
@@ -1521,7 +1576,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1521
1576
|
if end is not None:
|
|
1522
1577
|
params["end"] = end.isoformat()
|
|
1523
1578
|
if sports is not None:
|
|
1524
|
-
params["
|
|
1579
|
+
params["sport"] = self._enums_to_strings(sports)
|
|
1525
1580
|
if tags is not None:
|
|
1526
1581
|
params["tags"] = tags
|
|
1527
1582
|
|
|
@@ -1787,6 +1842,30 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1787
1842
|
self._raise_for_status(response)
|
|
1788
1843
|
return [UserSummary.model_validate(user) for user in response.json()]
|
|
1789
1844
|
|
|
1845
|
+
def get_team_user(
|
|
1846
|
+
self,
|
|
1847
|
+
*,
|
|
1848
|
+
team_id: str,
|
|
1849
|
+
user: str,
|
|
1850
|
+
search_mode: Literal["auto", "id", "name"] = "auto",
|
|
1851
|
+
) -> UserSummary:
|
|
1852
|
+
"""Get a team-authorized user by ID or name.
|
|
1853
|
+
|
|
1854
|
+
Args:
|
|
1855
|
+
team_id: The team's ID.
|
|
1856
|
+
user: User ID or (part of) display name.
|
|
1857
|
+
search_mode: "auto" (detect), "id", or "name".
|
|
1858
|
+
|
|
1859
|
+
Returns:
|
|
1860
|
+
UserSummary: The matching user.
|
|
1861
|
+
|
|
1862
|
+
Raises:
|
|
1863
|
+
ValueError: If no match or multiple matches found.
|
|
1864
|
+
HTTPStatusError: If the API request fails.
|
|
1865
|
+
"""
|
|
1866
|
+
users = self.get_team_users(team_id)
|
|
1867
|
+
return self._find_user(user, users, search_mode)
|
|
1868
|
+
|
|
1790
1869
|
def authorize_team(self, team_id: str, scopes: list[Scope | str] | None = None):
|
|
1791
1870
|
"""Authorizes a team to access the current user's data.
|
|
1792
1871
|
|
|
@@ -2018,6 +2097,7 @@ _generate_singleton_methods(
|
|
|
2018
2097
|
"get_users",
|
|
2019
2098
|
"create_user",
|
|
2020
2099
|
"get_team_users",
|
|
2100
|
+
"get_team_user",
|
|
2021
2101
|
"authorize_team",
|
|
2022
2102
|
"get_userinfo",
|
|
2023
2103
|
"whoami",
|
|
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.69.0 → sweatstack-0.71.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{sweatstack-0.69.0 → sweatstack-0.71.0}/playground/Sweat Stack examples/Getting started.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.69.0 → sweatstack-0.71.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
|