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.
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/client.md +7 -1
- {sweatstack-0.73.0 → sweatstack-0.74.0}/CHANGELOG.md +6 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/PKG-INFO +1 -1
- {sweatstack-0.73.0 → sweatstack-0.74.0}/pyproject.toml +1 -1
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/client.py +33 -2
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/openapi_schemas.py +26 -1
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/schemas.py +3 -2
- sweatstack-0.74.0/tests/test_teams.py +113 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/uv.lock +1 -1
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/settings.local.json +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.gitignore +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/.python-version +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/CONTRIBUTING.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/LICENSE +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/Makefile +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/README.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/docs/conf.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/docs/everything.rst +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/docs/index.rst +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/examples/fastapi_webhooks_example.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/plans/001a_tests.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/plans/001b_metadata.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/plans/001c_dailies.md +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/__init__.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_dailies.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_dtype_conversion.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_metadata.py +0 -0
- {sweatstack-0.73.0 → sweatstack-0.74.0}/tests/test_tests.py +0 -0
- {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
|
-
#
|
|
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
|
|
@@ -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,
|
|
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-
|
|
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,
|
|
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"
|
|
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.73.0 → sweatstack-0.74.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
|