sweatstack 0.69.0__tar.gz → 0.70.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.70.0}/.claude/skills/sweatstack-python/client.md +5 -1
- {sweatstack-0.69.0 → sweatstack-0.70.0}/CHANGELOG.md +9 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/PKG-INFO +1 -1
- {sweatstack-0.69.0 → sweatstack-0.70.0}/pyproject.toml +1 -1
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/client.py +64 -34
- {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/settings.local.json +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/.gitignore +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/.python-version +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/CLIENT_DTYPE_CONVERSION.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/CLIENT_LIBRARY_SKILL.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_DOCS.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_PLUGIN.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_USER_SWITCHING.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_WEBHOOKS.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/LOCAL_AUTH.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/Makefile +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/README.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/docs/conf.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/docs/everything.rst +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/docs/index.rst +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/examples/tokens.db +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/fastapi_coaching_example.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/fastapi_example.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/fastapi_sweatstack.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/README.md +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/Untitled.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/hello.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/pyproject.toml +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/tests/__init__.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/tests/test_webhooks.py +0 -0
- {sweatstack-0.69.0 → sweatstack-0.70.0}/uv.lock +0 -0
|
@@ -176,6 +176,7 @@ user = client.create_user(first_name="John", last_name="Doe")
|
|
|
176
176
|
|
|
177
177
|
# Team management
|
|
178
178
|
team_users = client.get_team_users(team_id="team_abc")
|
|
179
|
+
athlete = client.get_team_user(team_id="team_abc", user="john") # by name or ID
|
|
179
180
|
client.authorize_team(team_id="team_abc", scopes=[Scope.data_read])
|
|
180
181
|
```
|
|
181
182
|
|
|
@@ -187,9 +188,12 @@ Operate on behalf of another user (requires appropriate permissions).
|
|
|
187
188
|
|
|
188
189
|
```python
|
|
189
190
|
other = client.delegated_client("john")
|
|
190
|
-
other = client.delegated_client("john", team_id="team_abc") # via team
|
|
191
191
|
other_activities = other.get_activities()
|
|
192
192
|
# original client is unchanged
|
|
193
|
+
|
|
194
|
+
# Via team membership
|
|
195
|
+
athlete = client.get_team_user(team_id="team_abc", user="john")
|
|
196
|
+
other = client.delegated_client(athlete, team_id="team_abc")
|
|
193
197
|
```
|
|
194
198
|
|
|
195
199
|
`switch_user()` modifies the client in-place — useful in interactive/notebook contexts but avoid in scripts:
|
|
@@ -6,6 +6,15 @@ 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.70.0] - 2026-03-13
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Added `get_team_user(*, team_id, user, search_mode)` method to find a single team-authorized user by ID or name.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- 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()`.
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
## [0.69.0] - 2026-03-12
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -498,72 +498,77 @@ class _DelegationMixin:
|
|
|
498
498
|
|
|
499
499
|
return len(user) in (16, 26) and user.isalnum()
|
|
500
500
|
|
|
501
|
-
def
|
|
502
|
-
"""
|
|
501
|
+
def _find_user_by_name(self, name: str, users: list) -> UserSummary:
|
|
502
|
+
"""Find a user by name from a list of users.
|
|
503
503
|
|
|
504
504
|
Args:
|
|
505
|
-
name: The
|
|
505
|
+
name: The (partial) display name to search for.
|
|
506
|
+
users: The list of UserSummary objects to search.
|
|
506
507
|
|
|
507
508
|
Returns:
|
|
508
|
-
UserSummary: The user
|
|
509
|
+
UserSummary: The matching user.
|
|
509
510
|
|
|
510
511
|
Raises:
|
|
511
|
-
ValueError: If
|
|
512
|
-
ValueError: If multiple users are found with the same name.
|
|
512
|
+
ValueError: If no match or multiple matches found.
|
|
513
513
|
"""
|
|
514
|
-
matches = []
|
|
515
|
-
for user in self.get_users():
|
|
516
|
-
if name in user.display_name.lower():
|
|
517
|
-
matches.append(user)
|
|
514
|
+
matches = [u for u in users if name in u.display_name.lower()]
|
|
518
515
|
|
|
519
516
|
if len(matches) == 0:
|
|
520
517
|
raise ValueError(f"User with name {name} not found")
|
|
521
518
|
elif len(matches) > 1:
|
|
522
|
-
raise ValueError(f"Multiple users found with name {name}: {', '.join([
|
|
519
|
+
raise ValueError(f"Multiple users found with name {name}: {', '.join([u.display_name for u in matches])}")
|
|
523
520
|
return matches[0]
|
|
524
521
|
|
|
525
|
-
def
|
|
526
|
-
"""
|
|
522
|
+
def _find_user_by_id(self, id: str, users: list) -> UserSummary:
|
|
523
|
+
"""Find a user by ID from a list of users.
|
|
527
524
|
|
|
528
525
|
Args:
|
|
529
|
-
id: The ID
|
|
526
|
+
id: The user ID to search for.
|
|
527
|
+
users: The list of UserSummary objects to search.
|
|
530
528
|
|
|
531
529
|
Returns:
|
|
532
|
-
UserSummary: The user
|
|
530
|
+
UserSummary: The matching user, or None if not found.
|
|
531
|
+
"""
|
|
532
|
+
return next((u for u in users if u.id == id), None)
|
|
533
533
|
|
|
534
|
-
|
|
535
|
-
|
|
534
|
+
def _find_user(self, user: str, users: list, search_mode: Literal["auto", "id", "name"] = "auto") -> UserSummary:
|
|
535
|
+
"""Find a user by ID or name from a list of users.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
user: User ID or (part of) display name.
|
|
539
|
+
users: The list of UserSummary objects to search.
|
|
540
|
+
search_mode: "auto" (detect), "id", or "name".
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
UserSummary: The matching user.
|
|
536
544
|
"""
|
|
537
|
-
|
|
538
|
-
|
|
545
|
+
if search_mode == "auto":
|
|
546
|
+
if self._is_user_id(user):
|
|
547
|
+
return self._find_user_by_id(user, users)
|
|
548
|
+
else:
|
|
549
|
+
return self._find_user_by_name(user, users)
|
|
550
|
+
elif search_mode == "id":
|
|
551
|
+
return self._find_user_by_id(user, users)
|
|
552
|
+
elif search_mode == "name":
|
|
553
|
+
return self._find_user_by_name(user, users)
|
|
539
554
|
|
|
540
555
|
def get_user(self, user: str, *, search_mode: Literal["auto", "id", "name"] = "auto") -> UserSummary:
|
|
541
556
|
"""Get a user by ID or name.
|
|
542
557
|
This method will always authenticate as the principal user.
|
|
543
558
|
|
|
544
559
|
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.
|
|
560
|
+
user: User ID or (part of) display name.
|
|
561
|
+
search_mode: "auto" (detect), "id", or "name".
|
|
550
562
|
|
|
551
563
|
Returns:
|
|
552
564
|
UserSummary: The user object.
|
|
553
565
|
|
|
554
566
|
Raises:
|
|
555
|
-
|
|
567
|
+
ValueError: If no match or multiple matches found.
|
|
556
568
|
"""
|
|
557
569
|
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)
|
|
570
|
+
users = client.get_users()
|
|
571
|
+
return client._find_user(user, users, search_mode)
|
|
567
572
|
|
|
568
573
|
def switch_user(
|
|
569
574
|
self,
|
|
@@ -1787,6 +1792,30 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
|
|
|
1787
1792
|
self._raise_for_status(response)
|
|
1788
1793
|
return [UserSummary.model_validate(user) for user in response.json()]
|
|
1789
1794
|
|
|
1795
|
+
def get_team_user(
|
|
1796
|
+
self,
|
|
1797
|
+
*,
|
|
1798
|
+
team_id: str,
|
|
1799
|
+
user: str,
|
|
1800
|
+
search_mode: Literal["auto", "id", "name"] = "auto",
|
|
1801
|
+
) -> UserSummary:
|
|
1802
|
+
"""Get a team-authorized user by ID or name.
|
|
1803
|
+
|
|
1804
|
+
Args:
|
|
1805
|
+
team_id: The team's ID.
|
|
1806
|
+
user: User ID or (part of) display name.
|
|
1807
|
+
search_mode: "auto" (detect), "id", or "name".
|
|
1808
|
+
|
|
1809
|
+
Returns:
|
|
1810
|
+
UserSummary: The matching user.
|
|
1811
|
+
|
|
1812
|
+
Raises:
|
|
1813
|
+
ValueError: If no match or multiple matches found.
|
|
1814
|
+
HTTPStatusError: If the API request fails.
|
|
1815
|
+
"""
|
|
1816
|
+
users = self.get_team_users(team_id)
|
|
1817
|
+
return self._find_user(user, users, search_mode)
|
|
1818
|
+
|
|
1790
1819
|
def authorize_team(self, team_id: str, scopes: list[Scope | str] | None = None):
|
|
1791
1820
|
"""Authorizes a team to access the current user's data.
|
|
1792
1821
|
|
|
@@ -2018,6 +2047,7 @@ _generate_singleton_methods(
|
|
|
2018
2047
|
"get_users",
|
|
2019
2048
|
"create_user",
|
|
2020
2049
|
"get_team_users",
|
|
2050
|
+
"get_team_user",
|
|
2021
2051
|
"authorize_team",
|
|
2022
2052
|
"get_userinfo",
|
|
2023
2053
|
"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.70.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{sweatstack-0.69.0 → sweatstack-0.70.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.70.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
|