sweatstack 0.73.0__tar.gz → 0.74.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 (53) hide show
  1. {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/client.md +7 -1
  2. {sweatstack-0.73.0 → sweatstack-0.74.0}/CHANGELOG.md +6 -0
  3. {sweatstack-0.73.0 → sweatstack-0.74.0}/PKG-INFO +1 -1
  4. {sweatstack-0.73.0 → sweatstack-0.74.0}/pyproject.toml +1 -1
  5. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/client.py +33 -2
  6. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/openapi_schemas.py +26 -1
  7. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/schemas.py +3 -2
  8. sweatstack-0.74.0/tests/test_teams.py +113 -0
  9. {sweatstack-0.73.0 → sweatstack-0.74.0}/uv.lock +1 -1
  10. {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/settings.local.json +0 -0
  11. {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  12. {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
  13. {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
  14. {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  15. {sweatstack-0.73.0 → sweatstack-0.74.0}/.gitignore +0 -0
  16. {sweatstack-0.73.0 → sweatstack-0.74.0}/.python-version +0 -0
  17. {sweatstack-0.73.0 → sweatstack-0.74.0}/CONTRIBUTING.md +0 -0
  18. {sweatstack-0.73.0 → sweatstack-0.74.0}/DEVELOPMENT.md +0 -0
  19. {sweatstack-0.73.0 → sweatstack-0.74.0}/LICENSE +0 -0
  20. {sweatstack-0.73.0 → sweatstack-0.74.0}/Makefile +0 -0
  21. {sweatstack-0.73.0 → sweatstack-0.74.0}/README.md +0 -0
  22. {sweatstack-0.73.0 → sweatstack-0.74.0}/docs/conf.py +0 -0
  23. {sweatstack-0.73.0 → sweatstack-0.74.0}/docs/everything.rst +0 -0
  24. {sweatstack-0.73.0 → sweatstack-0.74.0}/docs/index.rst +0 -0
  25. {sweatstack-0.73.0 → sweatstack-0.74.0}/examples/fastapi_webhooks_example.py +0 -0
  26. {sweatstack-0.73.0 → sweatstack-0.74.0}/examples/send_webhook.py +0 -0
  27. {sweatstack-0.73.0 → sweatstack-0.74.0}/plans/001a_tests.md +0 -0
  28. {sweatstack-0.73.0 → sweatstack-0.74.0}/plans/001b_metadata.md +0 -0
  29. {sweatstack-0.73.0 → sweatstack-0.74.0}/plans/001c_dailies.md +0 -0
  30. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  31. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/__init__.py +0 -0
  32. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/cli.py +0 -0
  33. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/constants.py +0 -0
  34. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/__init__.py +0 -0
  35. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/config.py +0 -0
  36. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/dependencies.py +0 -0
  37. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/models.py +0 -0
  38. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/routes.py +0 -0
  39. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/session.py +0 -0
  40. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  41. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  42. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/ipython_init.py +0 -0
  43. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  44. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/py.typed +0 -0
  45. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/streamlit.py +0 -0
  46. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/sweatshell.py +0 -0
  47. {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/utils.py +0 -0
  48. {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/__init__.py +0 -0
  49. {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_dailies.py +0 -0
  50. {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_dtype_conversion.py +0 -0
  51. {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_metadata.py +0 -0
  52. {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_tests.py +0 -0
  53. {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_webhooks.py +0 -0
@@ -286,7 +286,13 @@ user = client.get_user("abc123", search_mode="id")
286
286
  # Create a managed user (no login credentials)
287
287
  user = client.create_user(first_name="John", last_name="Doe")
288
288
 
289
- # Team management
289
+ # List teams you're a member/owner of
290
+ teams = client.get_teams() # list[TeamResponse] with .role
291
+
292
+ # List teams you've authorized to access your data
293
+ authorized = client.get_authorized_teams() # list[AuthorizedTeamResponse] with .scopes
294
+
295
+ # Team user management
290
296
  team_users = client.get_team_users(team_id="team_abc")
291
297
  athlete = client.get_team_user(team_id="team_abc", user="john") # by name or ID
292
298
  client.authorize_team(team_id="team_abc", scopes=[Scope.data_read])
@@ -6,6 +6,12 @@ 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.74.0] - 2026-04-22
10
+
11
+ ### Added
12
+ - Team listing — `get_teams()` returns teams you own or belong to, `get_authorized_teams()` returns teams you've granted data access to.
13
+
14
+
9
15
  ## [0.73.0] - 2026-04-09
10
16
 
11
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.73.0
3
+ Version: 0.74.0
4
4
  Summary: The official Python client for SweatStack
5
5
  Project-URL: Homepage, https://sweatstack.no
6
6
  Project-URL: Documentation, https://developer.sweatstack.no/getting-started/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.73.0"
3
+ version = "0.74.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -29,9 +29,10 @@ from platformdirs import user_cache_dir, user_data_dir
29
29
 
30
30
  from .constants import DEFAULT_URL
31
31
  from .schemas import (
32
- ActivityDetails, ActivitySummary, BackfillStatus, DailyMeasure, DailyResponse,
32
+ ActivityDetails, ActivitySummary, ApplicationMemberRole, AuthorizedTeamResponse,
33
+ BackfillStatus, DailyMeasure, DailyResponse,
33
34
  Marker, Metric, Scope, Sport,
34
- TestDetails, TestResults, TestSummary, TokenResponse, TraceDetails,
35
+ TeamResponse, TestDetails, TestResults, TestSummary, TokenResponse, TraceDetails,
35
36
  UserInfoResponse, UserResponse, UserSummary
36
37
  )
37
38
  from .utils import convert_to_standard_dtypes, decode_jwt_body, make_dataframe_streamlit_compatible
@@ -2278,6 +2279,34 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
2278
2279
  self._raise_for_status(response)
2279
2280
  return UserResponse.model_validate(response.json())
2280
2281
 
2282
+ def get_teams(self) -> list[TeamResponse]:
2283
+ """Lists all teams the current user owns or is a member of.
2284
+
2285
+ Returns:
2286
+ list[TeamResponse]: Teams with the user's role (owner or member).
2287
+
2288
+ Raises:
2289
+ HTTPStatusError: If the API request fails.
2290
+ """
2291
+ with self._http_client() as client:
2292
+ response = client.get(url="/api/v1/teams/")
2293
+ self._raise_for_status(response)
2294
+ return [TeamResponse.model_validate(team) for team in response.json()]
2295
+
2296
+ def get_authorized_teams(self) -> list[AuthorizedTeamResponse]:
2297
+ """Lists all teams the current user has authorized to access their data.
2298
+
2299
+ Returns:
2300
+ list[AuthorizedTeamResponse]: Teams with their granted scopes.
2301
+
2302
+ Raises:
2303
+ HTTPStatusError: If the API request fails.
2304
+ """
2305
+ with self._http_client() as client:
2306
+ response = client.get(url="/api/v1/teams/authorized")
2307
+ self._raise_for_status(response)
2308
+ return [AuthorizedTeamResponse.model_validate(team) for team in response.json()]
2309
+
2281
2310
  def get_team_users(self, team_id: str) -> list[UserSummary]:
2282
2311
  """Lists all users who have authorized a team to access their data.
2283
2312
 
@@ -2553,6 +2582,8 @@ _generate_singleton_methods(
2553
2582
  "get_user",
2554
2583
  "get_users",
2555
2584
  "create_user",
2585
+ "get_teams",
2586
+ "get_authorized_teams",
2556
2587
  "get_team_users",
2557
2588
  "get_team_user",
2558
2589
  "authorize_team",
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: openapi.json
3
- # timestamp: 2026-04-09T11:13:41+00:00
3
+ # timestamp: 2026-04-22T12:23:13+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -64,6 +64,11 @@ class ApplicationListItem(BaseModel):
64
64
  page_slug: Optional[str] = Field(None, title='Page Slug')
65
65
 
66
66
 
67
+ class ApplicationMemberRole(Enum):
68
+ owner = 'owner'
69
+ member = 'member'
70
+
71
+
67
72
  class Prompt(Enum):
68
73
  none = 'none'
69
74
  login = 'login'
@@ -1043,6 +1048,16 @@ class TeamCreateOrUpdate(BaseModel):
1043
1048
  privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
1044
1049
 
1045
1050
 
1051
+ class TeamResponse(BaseModel):
1052
+ id: str = Field(..., title='Id')
1053
+ name: str = Field(..., title='Name')
1054
+ description: str = Field(..., title='Description')
1055
+ url: AnyUrl = Field(..., title='Url')
1056
+ image: AnyUrl = Field(..., title='Image')
1057
+ privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
1058
+ role: Optional[ApplicationMemberRole] = None
1059
+
1060
+
1046
1061
  class TemperatureSummary(BaseModel):
1047
1062
  mean: Optional[float] = Field(None, title='Mean')
1048
1063
  min: Optional[float] = Field(None, title='Min')
@@ -1256,6 +1271,16 @@ class AuthorizeTeamRequest(BaseModel):
1256
1271
  scopes: List[Scope] = Field(..., title='Scopes')
1257
1272
 
1258
1273
 
1274
+ class AuthorizedTeamResponse(BaseModel):
1275
+ id: str = Field(..., title='Id')
1276
+ name: str = Field(..., title='Name')
1277
+ description: str = Field(..., title='Description')
1278
+ url: AnyUrl = Field(..., title='Url')
1279
+ image: AnyUrl = Field(..., title='Image')
1280
+ privacy_statement: AnyUrl = Field(..., title='Privacy Statement')
1281
+ scopes: List[Scope] = Field(..., title='Scopes')
1282
+
1283
+
1259
1284
  class BodyAuthorizeOauthAuthorizePost(BaseModel):
1260
1285
  client_id: str = Field(..., title='Client Id')
1261
1286
  redirect_uri: Optional[str] = Field(None, title='Redirect Uri')
@@ -14,9 +14,10 @@ from enum import Enum
14
14
  from typing import List, Union
15
15
 
16
16
  from .openapi_schemas import (
17
- ActivityDetails, ActivitySummary, BackfillStatus, DailyMeasure, DailyResponse,
17
+ ActivityDetails, ActivitySummary, ApplicationMemberRole, AuthorizedTeamResponse,
18
+ BackfillStatus, DailyMeasure, DailyResponse,
18
19
  Marker, Metric, Scope, Sport,
19
- TestDetails, TestResults, TestSummary, TokenResponse, TraceDetails,
20
+ TeamResponse, TestDetails, TestResults, TestSummary, TokenResponse, TraceDetails,
20
21
  UserInfoResponse, UserResponse, UserSummary
21
22
  )
22
23
 
@@ -0,0 +1,113 @@
1
+ """Tests for the Teams listing functionality.
2
+
3
+ Tests schema round-trips and enum handling for TeamResponse and
4
+ AuthorizedTeamResponse without hitting the API.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from sweatstack import ApplicationMemberRole, AuthorizedTeamResponse, Scope, TeamResponse
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Fixtures
14
+ # ---------------------------------------------------------------------------
15
+
16
+
17
+ @pytest.fixture
18
+ def sample_team() -> TeamResponse:
19
+ return TeamResponse(
20
+ id="team_001",
21
+ name="Cycling Club",
22
+ description="Local cycling team",
23
+ url="https://example.com",
24
+ image="https://example.com/logo.png",
25
+ privacy_statement="https://example.com/privacy",
26
+ role=ApplicationMemberRole.owner,
27
+ )
28
+
29
+
30
+ @pytest.fixture
31
+ def sample_authorized_team() -> AuthorizedTeamResponse:
32
+ return AuthorizedTeamResponse(
33
+ id="team_002",
34
+ name="Coach Platform",
35
+ description="Coaching service",
36
+ url="https://coach.example.com",
37
+ image="https://coach.example.com/logo.png",
38
+ privacy_statement="https://coach.example.com/privacy",
39
+ scopes=[Scope.data_read],
40
+ )
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Schema round-trip tests
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ class TestSchemaRoundTrip:
49
+ def test_team_response_round_trip(self, sample_team: TeamResponse):
50
+ """TeamResponse should survive serialize -> deserialize."""
51
+ dumped = sample_team.model_dump()
52
+ restored = TeamResponse.model_validate(dumped)
53
+
54
+ assert restored.id == "team_001"
55
+ assert restored.name == "Cycling Club"
56
+ assert restored.role == ApplicationMemberRole.owner
57
+
58
+ def test_team_response_null_role(self):
59
+ """TeamResponse with null role should round-trip cleanly."""
60
+ team = TeamResponse(
61
+ id="team_001",
62
+ name="Test",
63
+ description="Test",
64
+ url="https://example.com",
65
+ image="https://example.com/img.png",
66
+ privacy_statement="https://example.com/privacy",
67
+ role=None,
68
+ )
69
+ dumped = team.model_dump()
70
+ restored = TeamResponse.model_validate(dumped)
71
+
72
+ assert restored.role is None
73
+
74
+ def test_authorized_team_response_round_trip(
75
+ self, sample_authorized_team: AuthorizedTeamResponse
76
+ ):
77
+ """AuthorizedTeamResponse should survive serialize -> deserialize."""
78
+ dumped = sample_authorized_team.model_dump()
79
+ restored = AuthorizedTeamResponse.model_validate(dumped)
80
+
81
+ assert restored.id == "team_002"
82
+ assert restored.name == "Coach Platform"
83
+ assert restored.scopes == [Scope.data_read]
84
+
85
+ def test_authorized_team_multiple_scopes(self):
86
+ """AuthorizedTeamResponse with multiple scopes should round-trip."""
87
+ team = AuthorizedTeamResponse(
88
+ id="team_003",
89
+ name="Full Access",
90
+ description="Test",
91
+ url="https://example.com",
92
+ image="https://example.com/img.png",
93
+ privacy_statement="https://example.com/privacy",
94
+ scopes=[Scope.data_read, Scope.data_write],
95
+ )
96
+ dumped = team.model_dump()
97
+ restored = AuthorizedTeamResponse.model_validate(dumped)
98
+
99
+ assert len(restored.scopes) == 2
100
+ assert Scope.data_read in restored.scopes
101
+ assert Scope.data_write in restored.scopes
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Enum tests
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ class TestApplicationMemberRole:
110
+ def test_role_values(self):
111
+ """ApplicationMemberRole should have owner and member."""
112
+ assert ApplicationMemberRole.owner.value == "owner"
113
+ assert ApplicationMemberRole.member.value == "member"
@@ -2456,7 +2456,7 @@ wheels = [
2456
2456
 
2457
2457
  [[package]]
2458
2458
  name = "sweatstack"
2459
- version = "0.72.0"
2459
+ version = "0.73.0"
2460
2460
  source = { editable = "." }
2461
2461
  dependencies = [
2462
2462
  { name = "email-validator" },
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