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.
Files changed (28) hide show
  1. sweatstack-0.44.0/CHANGELOG.md +18 -0
  2. {sweatstack-0.43.0 → sweatstack-0.44.0}/PKG-INFO +2 -1
  3. {sweatstack-0.43.0 → sweatstack-0.44.0}/pyproject.toml +2 -1
  4. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/client.py +92 -4
  5. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/ipython_init.py +1 -5
  6. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/jupyterlab_oauth2_startup.py +1 -4
  7. {sweatstack-0.43.0 → sweatstack-0.44.0}/.gitignore +0 -0
  8. {sweatstack-0.43.0 → sweatstack-0.44.0}/.python-version +0 -0
  9. {sweatstack-0.43.0 → sweatstack-0.44.0}/DEVELOPMENT.md +0 -0
  10. {sweatstack-0.43.0 → sweatstack-0.44.0}/Makefile +0 -0
  11. {sweatstack-0.43.0 → sweatstack-0.44.0}/README.md +0 -0
  12. {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  13. {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/README.md +0 -0
  14. {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  15. {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/Untitled.ipynb +0 -0
  16. {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/hello.py +0 -0
  17. {sweatstack-0.43.0 → sweatstack-0.44.0}/playground/pyproject.toml +0 -0
  18. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  19. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/__init__.py +0 -0
  20. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/cli.py +0 -0
  21. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/constants.py +0 -0
  22. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/openapi_schemas.py +0 -0
  23. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/py.typed +0 -0
  24. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/schemas.py +0 -0
  25. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/streamlit.py +0 -0
  26. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/sweatshell.py +0 -0
  27. {sweatstack-0.43.0 → sweatstack-0.44.0}/src/sweatstack/utils.py +0 -0
  28. {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.43.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.43.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 = os.getenv("SWEATSTACK_API_KEY")
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
- return os.getenv("SWEATSTACK_REFRESH_TOKEN")
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.login()
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.login()
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
File without changes