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.
Files changed (61) hide show
  1. {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/client.md +5 -1
  2. {sweatstack-0.69.0 → sweatstack-0.70.0}/CHANGELOG.md +9 -0
  3. {sweatstack-0.69.0 → sweatstack-0.70.0}/PKG-INFO +1 -1
  4. {sweatstack-0.69.0 → sweatstack-0.70.0}/pyproject.toml +1 -1
  5. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/client.py +64 -34
  6. {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/settings.local.json +0 -0
  7. {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  8. {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
  9. {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
  10. {sweatstack-0.69.0 → sweatstack-0.70.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  11. {sweatstack-0.69.0 → sweatstack-0.70.0}/.gitignore +0 -0
  12. {sweatstack-0.69.0 → sweatstack-0.70.0}/.python-version +0 -0
  13. {sweatstack-0.69.0 → sweatstack-0.70.0}/CLIENT_DTYPE_CONVERSION.md +0 -0
  14. {sweatstack-0.69.0 → sweatstack-0.70.0}/CLIENT_LIBRARY_SKILL.md +0 -0
  15. {sweatstack-0.69.0 → sweatstack-0.70.0}/DEVELOPMENT.md +0 -0
  16. {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_DOCS.md +0 -0
  17. {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_PLUGIN.md +0 -0
  18. {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_USER_SWITCHING.md +0 -0
  19. {sweatstack-0.69.0 → sweatstack-0.70.0}/FASTAPI_WEBHOOKS.md +0 -0
  20. {sweatstack-0.69.0 → sweatstack-0.70.0}/LOCAL_AUTH.md +0 -0
  21. {sweatstack-0.69.0 → sweatstack-0.70.0}/Makefile +0 -0
  22. {sweatstack-0.69.0 → sweatstack-0.70.0}/README.md +0 -0
  23. {sweatstack-0.69.0 → sweatstack-0.70.0}/docs/conf.py +0 -0
  24. {sweatstack-0.69.0 → sweatstack-0.70.0}/docs/everything.rst +0 -0
  25. {sweatstack-0.69.0 → sweatstack-0.70.0}/docs/index.rst +0 -0
  26. {sweatstack-0.69.0 → sweatstack-0.70.0}/examples/fastapi_webhooks_example.py +0 -0
  27. {sweatstack-0.69.0 → sweatstack-0.70.0}/examples/send_webhook.py +0 -0
  28. {sweatstack-0.69.0 → sweatstack-0.70.0}/examples/tokens.db +0 -0
  29. {sweatstack-0.69.0 → sweatstack-0.70.0}/fastapi_coaching_example.py +0 -0
  30. {sweatstack-0.69.0 → sweatstack-0.70.0}/fastapi_example.py +0 -0
  31. {sweatstack-0.69.0 → sweatstack-0.70.0}/fastapi_sweatstack.py +0 -0
  32. {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  33. {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/README.md +0 -0
  34. {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  35. {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/Untitled.ipynb +0 -0
  36. {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/hello.py +0 -0
  37. {sweatstack-0.69.0 → sweatstack-0.70.0}/playground/pyproject.toml +0 -0
  38. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  39. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/__init__.py +0 -0
  40. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/cli.py +0 -0
  41. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/constants.py +0 -0
  42. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/__init__.py +0 -0
  43. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/config.py +0 -0
  44. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/dependencies.py +0 -0
  45. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/models.py +0 -0
  46. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/routes.py +0 -0
  47. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/session.py +0 -0
  48. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  49. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  50. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/ipython_init.py +0 -0
  51. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  52. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/openapi_schemas.py +0 -0
  53. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/py.typed +0 -0
  54. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/schemas.py +0 -0
  55. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/streamlit.py +0 -0
  56. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/sweatshell.py +0 -0
  57. {sweatstack-0.69.0 → sweatstack-0.70.0}/src/sweatstack/utils.py +0 -0
  58. {sweatstack-0.69.0 → sweatstack-0.70.0}/tests/__init__.py +0 -0
  59. {sweatstack-0.69.0 → sweatstack-0.70.0}/tests/test_dtype_conversion.py +0 -0
  60. {sweatstack-0.69.0 → sweatstack-0.70.0}/tests/test_webhooks.py +0 -0
  61. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.69.0
3
+ Version: 0.70.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.69.0"
3
+ version = "0.70.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -498,72 +498,77 @@ class _DelegationMixin:
498
498
 
499
499
  return len(user) in (16, 26) and user.isalnum()
500
500
 
501
- def _get_user_by_name(self, name: str) -> UserSummary:
502
- """Get a user by name.
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 name of the user to get.
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 object.
509
+ UserSummary: The matching user.
509
510
 
510
511
  Raises:
511
- ValueError: If the user is not found.
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([user.display_name for user in matches])}")
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 _get_user_by_id(self, id: str) -> UserSummary:
526
- """Get a user by ID.
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 of the user to get.
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 object.
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
- Raises:
535
- HTTPStatusError: If the user is not found.
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
- # TODO: Implement this using a user detail endpoint
538
- return next((user for user in self.get_users() if user.id == id), None)
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: Either a UserSummary object or a string representing the user id or (part of) the user name to get.
546
- search_mode: The mode to use when searching for the user.
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
- HTTPStatusError: If the user is not found.
567
+ ValueError: If no match or multiple matches found.
556
568
  """
557
569
  client = self.principal_client()
558
- if search_mode == "auto":
559
- if client._is_user_id(user):
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