sweatstack 0.43.0__tar.gz → 0.44.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.44.0/CHANGELOG.md +18 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/PKG-INFO +2 -1
- {sweatstack-0.43.0 → sweatstack-0.44.0}/pyproject.toml +2 -1
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/client.py +92 -4
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/ipython_init.py +1 -5
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/jupyterlab_oauth2_startup.py +1 -4
- {sweatstack-0.43.0 → sweatstack-0.44.0}/.gitignore +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/.python-version +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/Makefile +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/README.md +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/README.md +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/Untitled.ipynb +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/hello.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/pyproject.toml +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.43.0 → sweatstack-0.44.0}/uv.lock +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.44.0] - 2025-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added support for persistent storage of API keys and refresh tokens.
|
|
13
|
+
- Added a new `ss.authenticate()` method that handles authentication comprehensively, including calling `ss.login()` when needed. This method is now the recommended way to authenticate the client.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Changed
|
|
17
|
+
|
|
18
|
+
- The `sweatlab` and `sweatshell` commands now use the new `ss.authenticate()` method.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sweatstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.44.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
|
|
7
7
|
Requires-Dist: httpx>=0.28.1
|
|
8
8
|
Requires-Dist: pandas>=2.2.3
|
|
9
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
9
10
|
Requires-Dist: pyarrow>=18.0.0
|
|
10
11
|
Requires-Dist: pydantic>=2.10.5
|
|
11
12
|
Provides-Extra: jupyter
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sweatstack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.44.0"
|
|
4
4
|
description = "The official Python client for SweatStack"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -10,6 +10,7 @@ requires-python = ">=3.9"
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"httpx>=0.28.1",
|
|
12
12
|
"pandas>=2.2.3",
|
|
13
|
+
"platformdirs>=4.0.0",
|
|
13
14
|
"pyarrow>=18.0.0",
|
|
14
15
|
"pydantic>=2.10.5",
|
|
15
16
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import contextlib
|
|
3
|
+
import json
|
|
3
4
|
import random
|
|
4
5
|
import hashlib
|
|
5
6
|
import logging
|
|
@@ -14,11 +15,13 @@ from functools import wraps
|
|
|
14
15
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
15
16
|
from importlib.metadata import version
|
|
16
17
|
from io import BytesIO
|
|
18
|
+
from pathlib import Path
|
|
17
19
|
from typing import Any, Generator, get_type_hints, List, Literal
|
|
18
20
|
from urllib.parse import parse_qs, urlparse
|
|
19
21
|
|
|
20
22
|
import httpx
|
|
21
23
|
import pandas as pd
|
|
24
|
+
from platformdirs import user_data_dir
|
|
22
25
|
|
|
23
26
|
from .constants import DEFAULT_URL
|
|
24
27
|
from .schemas import (
|
|
@@ -45,6 +48,45 @@ AUTH_SUCCESSFUL_RESPONSE = """<!DOCTYPE html>
|
|
|
45
48
|
OAUTH2_CLIENT_ID = "5382f68b0d254378"
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
class TokenStorageMixin:
|
|
52
|
+
"""Mixin for handling persistent token storage using platformdirs."""
|
|
53
|
+
|
|
54
|
+
def _get_token_file_path(self) -> Path:
|
|
55
|
+
"""Get the path to the token storage file."""
|
|
56
|
+
data_dir = user_data_dir("SweatStack", "SweatStack")
|
|
57
|
+
return Path(data_dir) / "tokens.json"
|
|
58
|
+
|
|
59
|
+
def _save_tokens(self, access_token: str, refresh_token: str) -> None:
|
|
60
|
+
"""Save tokens to the user data directory."""
|
|
61
|
+
token_file = self._get_token_file_path()
|
|
62
|
+
token_file.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
token_data = {
|
|
65
|
+
"access_token": access_token,
|
|
66
|
+
"refresh_token": refresh_token
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
with open(token_file, "w") as f:
|
|
70
|
+
json.dump(token_data, f, indent=2)
|
|
71
|
+
|
|
72
|
+
# Set restrictive permissions (user read/write only)
|
|
73
|
+
token_file.chmod(0o600)
|
|
74
|
+
|
|
75
|
+
def _load_persistent_tokens(self) -> tuple[str | None, str | None]:
|
|
76
|
+
"""Load tokens from the user data directory."""
|
|
77
|
+
token_file = self._get_token_file_path()
|
|
78
|
+
|
|
79
|
+
if not token_file.exists():
|
|
80
|
+
return None, None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with open(token_file, "r") as f:
|
|
84
|
+
token_data = json.load(f)
|
|
85
|
+
return token_data.get("access_token"), token_data.get("refresh_token")
|
|
86
|
+
except (json.JSONDecodeError, FileNotFoundError, KeyError):
|
|
87
|
+
return None, None
|
|
88
|
+
|
|
89
|
+
|
|
48
90
|
try:
|
|
49
91
|
__version__ = version("sweatstack")
|
|
50
92
|
except ImportError:
|
|
@@ -52,7 +94,7 @@ except ImportError:
|
|
|
52
94
|
|
|
53
95
|
|
|
54
96
|
class OAuth2Mixin:
|
|
55
|
-
def login(self):
|
|
97
|
+
def login(self, persist_api_key: bool = True):
|
|
56
98
|
"""Initiates the OAuth2 login flow for SweatStack authentication.
|
|
57
99
|
|
|
58
100
|
This method starts a local HTTP server to receive the OAuth2 callback,
|
|
@@ -62,6 +104,10 @@ class OAuth2Mixin:
|
|
|
62
104
|
The method uses PKCE (Proof Key for Code Exchange) for enhanced security
|
|
63
105
|
during the OAuth2 authorization code flow.
|
|
64
106
|
|
|
107
|
+
Args:
|
|
108
|
+
persist_api_key: Whether to save the API key to persistent storage for future use.
|
|
109
|
+
Defaults to True.
|
|
110
|
+
|
|
65
111
|
Returns:
|
|
66
112
|
None
|
|
67
113
|
|
|
@@ -144,10 +190,45 @@ class OAuth2Mixin:
|
|
|
144
190
|
self.jwt = token_response.get("access_token")
|
|
145
191
|
self.api_key = self.jwt
|
|
146
192
|
self.refresh_token = token_response.get("refresh_token")
|
|
193
|
+
|
|
194
|
+
if persist_api_key:
|
|
195
|
+
self._save_tokens(self.api_key, self.refresh_token)
|
|
147
196
|
print(f"SweatStack Python login successful.")
|
|
148
197
|
else:
|
|
149
198
|
raise Exception("SweatStack Python login failed. Please try again.")
|
|
150
199
|
|
|
200
|
+
def authenticate(self, *, persist_api_key: bool = True, force_login: bool = False) -> None:
|
|
201
|
+
"""Ensures the client is authenticated, either using existing tokens or by initiating login.
|
|
202
|
+
|
|
203
|
+
This method checks for authentication in the following order:
|
|
204
|
+
1. Current instance tokens (if already set)
|
|
205
|
+
2. Environment variables (SWEATSTACK_API_KEY, SWEATSTACK_REFRESH_TOKEN)
|
|
206
|
+
3. Persistent storage tokens
|
|
207
|
+
4. If none found or force_login is True, initiates OAuth2 login flow
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
persist_api_key: Whether to save tokens to persistent storage after login.
|
|
211
|
+
Defaults to True.
|
|
212
|
+
force_login: Whether to force a new login even if tokens are available.
|
|
213
|
+
Defaults to False.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
None
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
Exception: If the authentication process fails.
|
|
220
|
+
"""
|
|
221
|
+
if force_login:
|
|
222
|
+
self.login(persist_api_key=persist_api_key)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# Check if we already have valid tokens
|
|
226
|
+
if self.api_key:
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
# If no tokens available, initiate login
|
|
230
|
+
self.login(persist_api_key=persist_api_key)
|
|
231
|
+
|
|
151
232
|
|
|
152
233
|
class DelegationMixin:
|
|
153
234
|
def _validate_user(self, user: str | UserSummary):
|
|
@@ -352,7 +433,7 @@ class DelegationMixin:
|
|
|
352
433
|
)
|
|
353
434
|
|
|
354
435
|
|
|
355
|
-
class Client(OAuth2Mixin, DelegationMixin):
|
|
436
|
+
class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
|
|
356
437
|
def __init__(
|
|
357
438
|
self,
|
|
358
439
|
api_key: str | None = None,
|
|
@@ -399,8 +480,10 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
399
480
|
def api_key(self) -> str:
|
|
400
481
|
if self._api_key is not None:
|
|
401
482
|
value = self._api_key
|
|
483
|
+
elif value := os.getenv("SWEATSTACK_API_KEY"):
|
|
484
|
+
pass
|
|
402
485
|
else:
|
|
403
|
-
value =
|
|
486
|
+
value, _ = self._load_persistent_tokens()
|
|
404
487
|
|
|
405
488
|
if value is None:
|
|
406
489
|
# A non-authenticated client is a potentially valid use-case.
|
|
@@ -416,8 +499,12 @@ class Client(OAuth2Mixin, DelegationMixin):
|
|
|
416
499
|
def refresh_token(self) -> str:
|
|
417
500
|
if self._refresh_token is not None:
|
|
418
501
|
return self._refresh_token
|
|
502
|
+
elif value := os.getenv("SWEATSTACK_REFRESH_TOKEN"):
|
|
503
|
+
pass
|
|
419
504
|
else:
|
|
420
|
-
|
|
505
|
+
_, value = self._load_persistent_tokens()
|
|
506
|
+
|
|
507
|
+
return value
|
|
421
508
|
|
|
422
509
|
@refresh_token.setter
|
|
423
510
|
def refresh_token(self, value: str):
|
|
@@ -1156,6 +1243,7 @@ def _generate_singleton_methods(method_names: List[str]) -> None:
|
|
|
1156
1243
|
_generate_singleton_methods(
|
|
1157
1244
|
[
|
|
1158
1245
|
"login",
|
|
1246
|
+
"authenticate",
|
|
1159
1247
|
|
|
1160
1248
|
"get_user",
|
|
1161
1249
|
"get_users",
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import time
|
|
2
|
-
|
|
3
1
|
import sweatstack as ss
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
print("\n")
|
|
7
5
|
print(">>>>>>>>>> Sweat Stack Initialization <<<<<<<<<")
|
|
8
6
|
print("Initializing....")
|
|
9
|
-
print("You will be redirected to your browser for authentication.\n")
|
|
10
|
-
time.sleep(2)
|
|
11
7
|
|
|
12
|
-
ss.
|
|
8
|
+
ss.authenticate()
|
|
@@ -5,7 +5,6 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
import sweatstack as ss
|
|
7
7
|
from jupyterlab.labapp import LabApp
|
|
8
|
-
from sweatstack.client import _default_client
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
def start_jupyterlab_with_oauth():
|
|
@@ -24,9 +23,7 @@ def start_jupyterlab_with_oauth():
|
|
|
24
23
|
if not target_dir.exists():
|
|
25
24
|
shutil.copytree(examples_dir, target_dir)
|
|
26
25
|
|
|
27
|
-
ss.
|
|
28
|
-
os.environ["SWEATSTACK_API_KEY"] = _default_client.api_key
|
|
29
|
-
os.environ["SWEATSTACK_REFRESH_TOKEN"] = _default_client.refresh_token
|
|
26
|
+
ss.authenticate()
|
|
30
27
|
|
|
31
28
|
|
|
32
29
|
return LabApp.launch_instance(argv=remaining_args)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.43.0 → sweatstack-0.44.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{sweatstack-0.43.0 → sweatstack-0.44.0}/playground/Sweat Stack examples/Getting started.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.43.0 → sweatstack-0.44.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
|