sweatstack 0.40.1__tar.gz → 0.41.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.40.1 → sweatstack-0.41.0}/PKG-INFO +1 -1
- {sweatstack-0.40.1 → sweatstack-0.41.0}/pyproject.toml +1 -1
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/client.py +116 -8
- {sweatstack-0.40.1 → sweatstack-0.41.0}/.gitignore +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/.python-version +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/Makefile +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/README.md +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/playground/README.md +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/playground/Untitled.ipynb +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/playground/hello.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/playground/pyproject.toml +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.40.1 → sweatstack-0.41.0}/uv.lock +0 -0
|
@@ -167,14 +167,106 @@ class DelegationMixin:
|
|
|
167
167
|
|
|
168
168
|
return response.json()
|
|
169
169
|
|
|
170
|
-
def
|
|
170
|
+
def _is_user_id(self, user: str) -> bool:
|
|
171
|
+
"""Check if a string is a valid user ID.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
user: The string to check.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
bool: True if the string is a valid user ID format, False otherwise.
|
|
178
|
+
"""
|
|
179
|
+
if not isinstance(user, str):
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
return len(user) == 16 and user.isalnum()
|
|
183
|
+
|
|
184
|
+
def _get_user_by_name(self, name: str) -> UserSummary:
|
|
185
|
+
"""Get a user by name.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
name: The name of the user to get.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
UserSummary: The user object.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
ValueError: If the user is not found.
|
|
195
|
+
ValueError: If multiple users are found with the same name.
|
|
196
|
+
"""
|
|
197
|
+
matches = []
|
|
198
|
+
for user in self.get_users():
|
|
199
|
+
if name in user.display_name.lower():
|
|
200
|
+
matches.append(user)
|
|
201
|
+
|
|
202
|
+
if len(matches) == 0:
|
|
203
|
+
raise ValueError(f"User with name {name} not found")
|
|
204
|
+
elif len(matches) > 1:
|
|
205
|
+
raise ValueError(f"Multiple users found with name {name}: {', '.join([user.display_name for user in matches])}")
|
|
206
|
+
return matches[0]
|
|
207
|
+
|
|
208
|
+
def _get_user_by_id(self, id: str) -> UserSummary:
|
|
209
|
+
"""Get a user by ID.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
id: The ID of the user to get.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
UserSummary: The user object.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
HTTPStatusError: If the user is not found.
|
|
219
|
+
"""
|
|
220
|
+
# TODO: Implement this using a user detail endpoint
|
|
221
|
+
return next((user for user in self.get_users() if user.id == id), None)
|
|
222
|
+
|
|
223
|
+
def get_user(self, user: str, *, search_mode: Literal["auto", "id", "name"] = "auto") -> UserSummary:
|
|
224
|
+
"""Get a user by ID or name.
|
|
225
|
+
This method will always authenticate as the principal user.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
user: Either a UserSummary object or a string representing the user id or (part of) the user name to get.
|
|
229
|
+
search_mode: The mode to use when searching for the user.
|
|
230
|
+
- "auto": Automatically determine the search mode based on the type of user argument.
|
|
231
|
+
- "id": Search for the user by ID.
|
|
232
|
+
- "name": Search for the user by name.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
UserSummary: The user object.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
HTTPStatusError: If the user is not found.
|
|
239
|
+
"""
|
|
240
|
+
client = self.principal_client()
|
|
241
|
+
if search_mode == "auto":
|
|
242
|
+
if client._is_user_id(user):
|
|
243
|
+
return client._get_user_by_id(user)
|
|
244
|
+
else:
|
|
245
|
+
return client._get_user_by_name(user)
|
|
246
|
+
elif search_mode == "id":
|
|
247
|
+
return client._get_user_by_id(user)
|
|
248
|
+
elif search_mode == "name":
|
|
249
|
+
return client._get_user_by_name(user)
|
|
250
|
+
|
|
251
|
+
def switch_user(
|
|
252
|
+
self,
|
|
253
|
+
user: str | UserSummary,
|
|
254
|
+
*,
|
|
255
|
+
search_mode: Literal["auto", "id", "name"] = "auto",
|
|
256
|
+
):
|
|
171
257
|
"""Switches the client to operate on behalf of another user.
|
|
172
258
|
|
|
173
259
|
This method changes the current client's authentication to act on behalf of the specified user.
|
|
174
260
|
The client will use a delegated token for all subsequent API calls.
|
|
175
261
|
|
|
176
262
|
Args:
|
|
177
|
-
user: Either a UserSummary object or a string user
|
|
263
|
+
user: Either a UserSummary object or a string representing the user id or (part of) the user name to switch to.
|
|
264
|
+
|
|
265
|
+
search_mode:
|
|
266
|
+
The mode to use when searching for the user.
|
|
267
|
+
- "auto": Automatically determine the search mode based on the type of user argument.
|
|
268
|
+
- "id": Search for the user by ID.
|
|
269
|
+
- "name": Search for the user by name.
|
|
178
270
|
|
|
179
271
|
Returns:
|
|
180
272
|
None
|
|
@@ -182,6 +274,11 @@ class DelegationMixin:
|
|
|
182
274
|
Raises:
|
|
183
275
|
HTTPStatusError: If the delegation request fails.
|
|
184
276
|
"""
|
|
277
|
+
self.switch_back()
|
|
278
|
+
|
|
279
|
+
if not isinstance(user, UserSummary):
|
|
280
|
+
user = self.get_user(user, search_mode=search_mode)
|
|
281
|
+
|
|
185
282
|
token_response = self._get_delegated_token(user)
|
|
186
283
|
self.api_key = token_response["access_token"]
|
|
187
284
|
self.refresh_token = token_response["refresh_token"]
|
|
@@ -268,14 +365,14 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
268
365
|
self.url = url
|
|
269
366
|
self.streamlit_compatible = streamlit_compatible
|
|
270
367
|
|
|
271
|
-
def _do_token_refresh(self,
|
|
368
|
+
def _do_token_refresh(self, tz: str) -> str:
|
|
272
369
|
with self._http_client() as client:
|
|
273
370
|
response = client.post(
|
|
274
371
|
"/api/v1/oauth/token",
|
|
275
372
|
json={
|
|
276
373
|
"grant_type": "refresh_token",
|
|
277
374
|
"refresh_token": self.refresh_token,
|
|
278
|
-
"
|
|
375
|
+
"tz": tz,
|
|
279
376
|
},
|
|
280
377
|
)
|
|
281
378
|
|
|
@@ -289,7 +386,7 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
289
386
|
TOKEN_EXPIRY_MARGIN = 5
|
|
290
387
|
if body["exp"] - TOKEN_EXPIRY_MARGIN < time.time():
|
|
291
388
|
# Token is (almost) expired, refresh it
|
|
292
|
-
token = self._do_token_refresh(body["
|
|
389
|
+
token = self._do_token_refresh(body["tz"])
|
|
293
390
|
self._api_key = token
|
|
294
391
|
except Exception:
|
|
295
392
|
# If token can't be decoded, just return as-is
|
|
@@ -359,6 +456,14 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
359
456
|
with httpx.Client(base_url=self.url, headers=headers) as client:
|
|
360
457
|
yield client
|
|
361
458
|
|
|
459
|
+
def _print_response_and_raise(self, response: httpx.Response):
|
|
460
|
+
try:
|
|
461
|
+
response.raise_for_status()
|
|
462
|
+
except httpx.HTTPStatusError as exception:
|
|
463
|
+
additional_info = response.text
|
|
464
|
+
exception.add_note(additional_info)
|
|
465
|
+
raise exception
|
|
466
|
+
|
|
362
467
|
def _raise_for_status(self, response: httpx.Response):
|
|
363
468
|
if response.status_code == 422:
|
|
364
469
|
raise ValueError(response.json())
|
|
@@ -366,7 +471,7 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
366
471
|
try:
|
|
367
472
|
import streamlit
|
|
368
473
|
except ImportError:
|
|
369
|
-
|
|
474
|
+
self._print_response_and_raise(response)
|
|
370
475
|
else:
|
|
371
476
|
try:
|
|
372
477
|
response.raise_for_status()
|
|
@@ -380,7 +485,7 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
380
485
|
raise
|
|
381
486
|
|
|
382
487
|
else:
|
|
383
|
-
|
|
488
|
+
self._print_response_and_raise(response)
|
|
384
489
|
|
|
385
490
|
def _enums_to_strings(self, values: list[Enum | str]) -> list[str]:
|
|
386
491
|
return [value.value if isinstance(value, Enum) else value for value in values]
|
|
@@ -976,6 +1081,7 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
976
1081
|
This method retrieves all users that the current user has access to view.
|
|
977
1082
|
For regular users, this typically returns only their own user information.
|
|
978
1083
|
For admin users, this may return information about multiple users.
|
|
1084
|
+
This method will always authenticate as the principal user.
|
|
979
1085
|
|
|
980
1086
|
Returns:
|
|
981
1087
|
list[UserSummary]: A list of UserSummary objects containing basic user information.
|
|
@@ -983,7 +1089,8 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
983
1089
|
Raises:
|
|
984
1090
|
HTTPStatusError: If the API request fails.
|
|
985
1091
|
"""
|
|
986
|
-
|
|
1092
|
+
client = self.principal_client()
|
|
1093
|
+
with client._http_client() as client:
|
|
987
1094
|
response = client.get(
|
|
988
1095
|
url="/api/v1/users/",
|
|
989
1096
|
)
|
|
@@ -1050,6 +1157,7 @@ _generate_singleton_methods(
|
|
|
1050
1157
|
[
|
|
1051
1158
|
"login",
|
|
1052
1159
|
|
|
1160
|
+
"get_user",
|
|
1053
1161
|
"get_users",
|
|
1054
1162
|
"get_userinfo",
|
|
1055
1163
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.40.1 → sweatstack-0.41.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{sweatstack-0.40.1 → sweatstack-0.41.0}/playground/Sweat Stack examples/Getting started.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.40.1 → sweatstack-0.41.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
|