sweatstack 0.16.0__tar.gz → 0.18.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.16.0 → sweatstack-0.18.0}/PKG-INFO +1 -1
- {sweatstack-0.16.0 → sweatstack-0.18.0}/pyproject.toml +1 -1
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/client.py +68 -3
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/openapi_schemas.py +31 -17
- sweatstack-0.18.0/src/sweatstack/schemas.py +3 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/streamlit.py +1 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/uv.lock +1 -1
- sweatstack-0.16.0/src/sweatstack/schemas.py +0 -3
- {sweatstack-0.16.0 → sweatstack-0.18.0}/.gitignore +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/.python-version +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/Makefile +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/README.md +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.16.0 → sweatstack-0.18.0}/src/sweatstack/utils.py +0 -0
|
@@ -18,7 +18,9 @@ import httpx
|
|
|
18
18
|
import pandas as pd
|
|
19
19
|
|
|
20
20
|
from .constants import DEFAULT_URL
|
|
21
|
-
from .schemas import
|
|
21
|
+
from .schemas import (
|
|
22
|
+
ActivityDetails, ActivitySummary, Sport, TraceDetails, UserSummary
|
|
23
|
+
)
|
|
22
24
|
from .utils import decode_jwt_body, make_dataframe_streamlit_compatible
|
|
23
25
|
|
|
24
26
|
|
|
@@ -62,6 +64,7 @@ class OAuth2Mixin:
|
|
|
62
64
|
"redirect_uri": redirect_uri,
|
|
63
65
|
"code_challenge": code_challenge,
|
|
64
66
|
"scope": "data:read",
|
|
67
|
+
"prompt": "none",
|
|
65
68
|
}
|
|
66
69
|
base_url = self.url
|
|
67
70
|
path = "/oauth/authorize"
|
|
@@ -83,7 +86,7 @@ class OAuth2Mixin:
|
|
|
83
86
|
"grant_type": "authorization_code",
|
|
84
87
|
"client_id": OAUTH2_CLIENT_ID,
|
|
85
88
|
"code": server.code,
|
|
86
|
-
"code_verifier": code_verifier
|
|
89
|
+
"code_verifier": code_verifier,
|
|
87
90
|
}
|
|
88
91
|
response = httpx.post(
|
|
89
92
|
f"{self.url}/api/v1/oauth/token",
|
|
@@ -103,7 +106,55 @@ class OAuth2Mixin:
|
|
|
103
106
|
raise Exception("SweatStack Python login failed. Please try again.")
|
|
104
107
|
|
|
105
108
|
|
|
106
|
-
class
|
|
109
|
+
class DelegationMixin:
|
|
110
|
+
def _get_delegated_token(self, user_id: str):
|
|
111
|
+
with self._http_client() as client:
|
|
112
|
+
response = client.post(
|
|
113
|
+
"/api/v1/oauth/delegated-token",
|
|
114
|
+
json={"sub": user_id},
|
|
115
|
+
)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
def switch_user(self, user_id: str):
|
|
121
|
+
token_response = self._get_delegated_token(user_id)
|
|
122
|
+
self.api_key = token_response["access_token"]
|
|
123
|
+
self.refresh_token = token_response["refresh_token"]
|
|
124
|
+
|
|
125
|
+
def _get_principal_token(self):
|
|
126
|
+
with self._http_client() as client:
|
|
127
|
+
response = client.get(
|
|
128
|
+
"/api/v1/oauth/principal-token",
|
|
129
|
+
)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
return response.json()
|
|
132
|
+
|
|
133
|
+
def switch_back(self):
|
|
134
|
+
token_response = self._get_principal_token()
|
|
135
|
+
self.api_key = token_response["access_token"]
|
|
136
|
+
self.refresh_token = token_response["refresh_token"]
|
|
137
|
+
|
|
138
|
+
def delegated_client(self, user_id: str):
|
|
139
|
+
token_response = self._get_delegated_token(user_id)
|
|
140
|
+
return self.__class__(
|
|
141
|
+
api_key=token_response["access_token"],
|
|
142
|
+
refresh_token=token_response["refresh_token"],
|
|
143
|
+
url=self.url,
|
|
144
|
+
streamlit_compatible=self.streamlit_compatible,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def principal_client(self):
|
|
148
|
+
token_response = self._get_principal_token()
|
|
149
|
+
return self.__class__(
|
|
150
|
+
api_key=token_response["access_token"],
|
|
151
|
+
refresh_token=token_response["refresh_token"],
|
|
152
|
+
url=self.url,
|
|
153
|
+
streamlit_compatible=self.streamlit_compatible,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Client(OAuth2Mixin, DelegationMixin):
|
|
107
158
|
def __init__(
|
|
108
159
|
self,
|
|
109
160
|
api_key: str | None = None,
|
|
@@ -553,6 +604,13 @@ class Client(OAuth2Mixin):
|
|
|
553
604
|
response.raise_for_status()
|
|
554
605
|
return response.json()
|
|
555
606
|
|
|
607
|
+
def list_users(self) -> list[UserSummary]:
|
|
608
|
+
with self._http_client() as client:
|
|
609
|
+
response = client.get(
|
|
610
|
+
url="/api/v1/users/",
|
|
611
|
+
)
|
|
612
|
+
response.raise_for_status()
|
|
613
|
+
return [UserSummary.model_validate(user) for user in response.json()]
|
|
556
614
|
|
|
557
615
|
_default_client = Client()
|
|
558
616
|
|
|
@@ -593,6 +651,8 @@ _generate_singleton_methods(
|
|
|
593
651
|
[
|
|
594
652
|
"login",
|
|
595
653
|
|
|
654
|
+
"list_users",
|
|
655
|
+
|
|
596
656
|
"get_activities",
|
|
597
657
|
|
|
598
658
|
"get_activity",
|
|
@@ -611,5 +671,10 @@ _generate_singleton_methods(
|
|
|
611
671
|
|
|
612
672
|
"get_sports",
|
|
613
673
|
"get_tags",
|
|
674
|
+
|
|
675
|
+
"switch_user",
|
|
676
|
+
"switch_back",
|
|
677
|
+
"delegated_client",
|
|
678
|
+
"principal_client",
|
|
614
679
|
]
|
|
615
680
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: openapi.json
|
|
3
|
-
# timestamp: 2025-
|
|
3
|
+
# timestamp: 2025-03-03T13:21:38+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -59,16 +59,6 @@ class BodySaveOrUpdateIntegrationProviderTenantsTenantIdIntegrationProvidersInte
|
|
|
59
59
|
redirect_url: str = Field(..., title='Redirect Url')
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
class BodyTokenOauthTokenPost(BaseModel):
|
|
63
|
-
grant_type: Literal['authorization_code'] = Field(..., title='Grant Type')
|
|
64
|
-
code: str = Field(..., title='Code')
|
|
65
|
-
client_id: str = Field(..., title='Client Id')
|
|
66
|
-
client_secret: Optional[str] = Field(None, title='Client Secret')
|
|
67
|
-
code_verifier: Optional[str] = Field(None, title='Code Verifier')
|
|
68
|
-
refresh_token: Optional[str] = Field(None, title='Refresh Token')
|
|
69
|
-
tz_offset: Optional[int] = Field(0, title='Tz Offset')
|
|
70
|
-
|
|
71
|
-
|
|
72
62
|
class BodyUpdateApplicationApplicationsApplicationIdPut(BaseModel):
|
|
73
63
|
name: str = Field(..., title='Name')
|
|
74
64
|
description: str = Field(..., title='Description')
|
|
@@ -122,7 +112,6 @@ class GarminFileTypes(Enum):
|
|
|
122
112
|
class GrantType(Enum):
|
|
123
113
|
authorization_code = 'authorization_code'
|
|
124
114
|
refresh_token = 'refresh_token'
|
|
125
|
-
implicit = 'implicit'
|
|
126
115
|
|
|
127
116
|
|
|
128
117
|
class HeartRateSummary(BaseModel):
|
|
@@ -166,6 +155,8 @@ class Scope(Enum):
|
|
|
166
155
|
data_read = 'data:read'
|
|
167
156
|
data_write = 'data:write'
|
|
168
157
|
admin = 'admin'
|
|
158
|
+
openid = 'openid'
|
|
159
|
+
profile = 'profile'
|
|
169
160
|
|
|
170
161
|
|
|
171
162
|
class Smo2Summary(BaseModel):
|
|
@@ -233,13 +224,12 @@ class TemperatureSummary(BaseModel):
|
|
|
233
224
|
|
|
234
225
|
class TokenRequest(BaseModel):
|
|
235
226
|
grant_type: GrantType
|
|
227
|
+
client_id: Optional[str] = Field(None, title='Client Id')
|
|
236
228
|
code: Optional[str] = Field(None, title='Code')
|
|
229
|
+
client_secret: Optional[str] = Field(None, title='Client Secret')
|
|
230
|
+
code_verifier: Optional[str] = Field(None, title='Code Verifier')
|
|
237
231
|
refresh_token: Optional[str] = Field(None, title='Refresh Token')
|
|
238
|
-
|
|
239
|
-
client_secret: Optional[SecretStr] = Field(None, title='Client Secret')
|
|
240
|
-
tz_offset: Optional[int] = Field(
|
|
241
|
-
3600, description='Timezone DST offset in seconds', title='Tz Offset'
|
|
242
|
-
)
|
|
232
|
+
tz_offset: Optional[int] = Field(0, title='Tz Offset')
|
|
243
233
|
|
|
244
234
|
|
|
245
235
|
class TokenResponse(BaseModel):
|
|
@@ -248,6 +238,7 @@ class TokenResponse(BaseModel):
|
|
|
248
238
|
expires_in: int = Field(..., title='Expires In')
|
|
249
239
|
refresh_token: str = Field(..., title='Refresh Token')
|
|
250
240
|
scope: Optional[str] = Field(None, title='Scope')
|
|
241
|
+
id_token: Optional[str] = Field(None, title='Id Token')
|
|
251
242
|
|
|
252
243
|
|
|
253
244
|
class TraceCreateOrUpdate(BaseModel):
|
|
@@ -259,6 +250,22 @@ class TraceCreateOrUpdate(BaseModel):
|
|
|
259
250
|
speed: Optional[confloat(ge=0.0)] = Field(None, title='Speed')
|
|
260
251
|
heart_rate: Optional[conint(ge=0)] = Field(None, title='Heart Rate')
|
|
261
252
|
tags: Optional[List[str]] = Field(None, title='Tags')
|
|
253
|
+
sport: Optional[Sport] = None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class UserInfoResponse(BaseModel):
|
|
257
|
+
sub: str = Field(..., title='Sub')
|
|
258
|
+
given_name: Optional[str] = Field(None, title='Given Name')
|
|
259
|
+
family_name: Optional[str] = Field(None, title='Family Name')
|
|
260
|
+
name: str = Field(..., title='Name')
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class UserSummary(BaseModel):
|
|
264
|
+
id: str = Field(..., title='Id')
|
|
265
|
+
first_name: Optional[str] = Field(..., title='First Name')
|
|
266
|
+
last_name: Optional[str] = Field(..., title='Last Name')
|
|
267
|
+
scopes: List[Scope] = Field(..., title='Scopes')
|
|
268
|
+
display_name: str = Field(..., title='Display Name')
|
|
262
269
|
|
|
263
270
|
|
|
264
271
|
class ValidationError(BaseModel):
|
|
@@ -283,10 +290,16 @@ class BodyAuthorizeOauthAuthorizePost(BaseModel):
|
|
|
283
290
|
redirect_uri: Optional[str] = Field(None, title='Redirect Uri')
|
|
284
291
|
scopes: List[Scope] = Field(..., min_length=1, title='Scopes')
|
|
285
292
|
state: Optional[str] = Field(None, title='State')
|
|
293
|
+
nonce: Optional[str] = Field(None, title='Nonce')
|
|
286
294
|
code_challenge: Optional[str] = Field(None, title='Code Challenge')
|
|
287
295
|
code_challenge_method: Optional[str] = Field(None, title='Code Challenge Method')
|
|
288
296
|
|
|
289
297
|
|
|
298
|
+
class DelegatedTokenRequest(BaseModel):
|
|
299
|
+
sub: str = Field(..., title='Sub')
|
|
300
|
+
scopes: List[Scope] = Field(..., title='Scopes')
|
|
301
|
+
|
|
302
|
+
|
|
290
303
|
class GarminActivityFileData(BaseModel):
|
|
291
304
|
userId: str = Field(..., title='Userid')
|
|
292
305
|
userAccessToken: str = Field(..., title='Useraccesstoken')
|
|
@@ -362,6 +375,7 @@ class TraceDetails(BaseModel):
|
|
|
362
375
|
heart_rate: Optional[conint(ge=0)] = Field(None, title='Heart Rate')
|
|
363
376
|
lap: Optional[Lap] = None
|
|
364
377
|
activity: Optional[ActivitySummary] = None
|
|
378
|
+
sport: Optional[Sport] = None
|
|
365
379
|
|
|
366
380
|
|
|
367
381
|
ActivityDetails.model_rebuild()
|
|
@@ -113,6 +113,7 @@ class StreamlitAuth:
|
|
|
113
113
|
|
|
114
114
|
def authenticate(self):
|
|
115
115
|
if self.is_authenticated():
|
|
116
|
+
st.toast("SweatStack authentication successful!", icon="✅")
|
|
116
117
|
self._show_sweatstack_logout()
|
|
117
118
|
elif code := st.query_params.get("code"):
|
|
118
119
|
self._exchange_token(code)
|
|
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
|