sweatstack 0.47.0__tar.gz → 0.48.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.48.0/.claude/settings.local.json +8 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/CHANGELOG.md +7 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/PKG-INFO +1 -1
- {sweatstack-0.47.0 → sweatstack-0.48.0}/pyproject.toml +1 -1
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/client.py +119 -2
- {sweatstack-0.47.0 → sweatstack-0.48.0}/.gitignore +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/.python-version +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/Makefile +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/README.md +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/playground/README.md +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/playground/Untitled.ipynb +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/playground/hello.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/playground/pyproject.toml +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.47.0 → sweatstack-0.48.0}/uv.lock +0 -0
|
@@ -6,6 +6,13 @@ 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.48.0] - 2025-08-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added optional local caching of longitudinal data, enabled by setting the `SWEATSTACK_CACHE_ENABLED` environment variable to `true`. The cache directory can be specified by setting the `SWEATSTACK_CACHE_DIR` environment variable. The cache can be cleared by calling `ss.clear_cache()`.
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
## [0.47.0] - 2025-08-07
|
|
10
17
|
|
|
11
18
|
### Added
|
|
@@ -6,6 +6,8 @@ import hashlib
|
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
8
|
import secrets
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
9
11
|
import time
|
|
10
12
|
import urllib
|
|
11
13
|
import webbrowser
|
|
@@ -49,6 +51,110 @@ AUTH_SUCCESSFUL_RESPONSE = """<!DOCTYPE html>
|
|
|
49
51
|
OAUTH2_CLIENT_ID = "5382f68b0d254378"
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
class LocalCacheMixin:
|
|
55
|
+
"""Mixin for handling local filesystem caching of API responses."""
|
|
56
|
+
|
|
57
|
+
def _cache_enabled(self) -> bool:
|
|
58
|
+
"""Check if local caching is enabled."""
|
|
59
|
+
return bool(os.getenv("SWEATSTACK_LOCAL_CACHE"))
|
|
60
|
+
|
|
61
|
+
def _log_cache_error(self, operation: str, error: Exception) -> None:
|
|
62
|
+
"""Log cache operation errors with context."""
|
|
63
|
+
cache_location = os.getenv("SWEATSTACK_CACHE_DIR") or tempfile.gettempdir()
|
|
64
|
+
try:
|
|
65
|
+
user_id = self._get_user_id_from_token()
|
|
66
|
+
except Exception:
|
|
67
|
+
user_id = "unknown"
|
|
68
|
+
|
|
69
|
+
logging.warning(
|
|
70
|
+
f"Failed to {operation} cache despite SWEATSTACK_LOCAL_CACHE being enabled. "
|
|
71
|
+
f"Cache directory: {cache_location}/sweatstack/{user_id}. "
|
|
72
|
+
f"Error: {error}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _get_user_id_from_token(self) -> str:
|
|
76
|
+
"""Extract user ID from the JWT token."""
|
|
77
|
+
if not self.api_key:
|
|
78
|
+
raise ValueError("Not authenticated. Please call authenticate() or login() first.")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
jwt_body = decode_jwt_body(self.api_key)
|
|
82
|
+
user_id = jwt_body.get("sub")
|
|
83
|
+
if not user_id:
|
|
84
|
+
raise ValueError("Unable to extract user ID from token")
|
|
85
|
+
return user_id
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise ValueError(f"Invalid authentication token: {e}")
|
|
88
|
+
|
|
89
|
+
def _get_cache_dir(self) -> Path:
|
|
90
|
+
"""Get cache directory for current user."""
|
|
91
|
+
user_id = self._get_user_id_from_token()
|
|
92
|
+
|
|
93
|
+
if cache_location := os.getenv("SWEATSTACK_CACHE_DIR"):
|
|
94
|
+
cache_dir = Path(cache_location) / user_id
|
|
95
|
+
else:
|
|
96
|
+
cache_dir = Path(tempfile.gettempdir()) / "sweatstack" / user_id
|
|
97
|
+
|
|
98
|
+
cache_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
99
|
+
return cache_dir
|
|
100
|
+
|
|
101
|
+
def _generate_longitudinal_cache_key(self, **params) -> str:
|
|
102
|
+
"""Generate cache key for longitudinal data requests."""
|
|
103
|
+
normalized_params = {}
|
|
104
|
+
|
|
105
|
+
for key, value in params.items():
|
|
106
|
+
if value is None:
|
|
107
|
+
continue
|
|
108
|
+
elif isinstance(value, list):
|
|
109
|
+
normalized_params[key] = sorted([
|
|
110
|
+
v.value if hasattr(v, 'value') else str(v) for v in value
|
|
111
|
+
])
|
|
112
|
+
elif hasattr(value, 'value'):
|
|
113
|
+
normalized_params[key] = value.value
|
|
114
|
+
elif isinstance(value, (date, datetime)):
|
|
115
|
+
normalized_params[key] = value.isoformat()
|
|
116
|
+
else:
|
|
117
|
+
normalized_params[key] = str(value)
|
|
118
|
+
|
|
119
|
+
cache_data = f"longitudinal_data:{json.dumps(normalized_params, sort_keys=True)}"
|
|
120
|
+
return hashlib.sha256(cache_data.encode()).hexdigest()[:16]
|
|
121
|
+
|
|
122
|
+
def _read_longitudinal_cache(self, cache_key: str) -> pd.DataFrame | None:
|
|
123
|
+
"""Try to read cached longitudinal data."""
|
|
124
|
+
try:
|
|
125
|
+
cache_dir = self._get_cache_dir()
|
|
126
|
+
cache_file = cache_dir / f"longitudinal-{cache_key}.parquet"
|
|
127
|
+
|
|
128
|
+
if cache_file.exists():
|
|
129
|
+
return pd.read_parquet(cache_file)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self._log_cache_error("read", e)
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def _write_longitudinal_cache(self, cache_key: str, content: bytes) -> None:
|
|
136
|
+
"""Write longitudinal data to cache."""
|
|
137
|
+
try:
|
|
138
|
+
cache_dir = self._get_cache_dir()
|
|
139
|
+
cache_file = cache_dir / f"longitudinal-{cache_key}.parquet"
|
|
140
|
+
cache_file.write_bytes(content)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
self._log_cache_error("write", e)
|
|
143
|
+
|
|
144
|
+
def clear_cache(self) -> None:
|
|
145
|
+
"""Clear all cached data for the current user.
|
|
146
|
+
|
|
147
|
+
This removes all cached data (longitudinal data, etc.) from the temporary
|
|
148
|
+
directory for the currently authenticated user.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
cache_dir = self._get_cache_dir()
|
|
152
|
+
if cache_dir.exists():
|
|
153
|
+
shutil.rmtree(cache_dir)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self._log_cache_error("clear", e)
|
|
156
|
+
|
|
157
|
+
|
|
52
158
|
class TokenStorageMixin:
|
|
53
159
|
"""Mixin for handling persistent token storage using platformdirs."""
|
|
54
160
|
|
|
@@ -434,7 +540,7 @@ class DelegationMixin:
|
|
|
434
540
|
)
|
|
435
541
|
|
|
436
542
|
|
|
437
|
-
class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
|
|
543
|
+
class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
|
|
438
544
|
def __init__(
|
|
439
545
|
self,
|
|
440
546
|
api_key: str | None = None,
|
|
@@ -891,6 +997,12 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
|
|
|
891
997
|
if adaptive_sampling_on is not None:
|
|
892
998
|
params["adaptive_sampling_on"] = self._enums_to_strings([adaptive_sampling_on])[0]
|
|
893
999
|
|
|
1000
|
+
if self._cache_enabled():
|
|
1001
|
+
cache_key = self._generate_longitudinal_cache_key(**params)
|
|
1002
|
+
cached_df = self._read_longitudinal_cache(cache_key)
|
|
1003
|
+
if cached_df is not None:
|
|
1004
|
+
return self._postprocess_dataframe(cached_df)
|
|
1005
|
+
|
|
894
1006
|
with self._http_client() as client:
|
|
895
1007
|
response = client.get(
|
|
896
1008
|
url="/api/v1/activities/longitudinal-data",
|
|
@@ -898,8 +1010,12 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
|
|
|
898
1010
|
)
|
|
899
1011
|
self._raise_for_status(response)
|
|
900
1012
|
|
|
1013
|
+
if self._cache_enabled():
|
|
1014
|
+
self._write_longitudinal_cache(cache_key, response.content)
|
|
1015
|
+
|
|
901
1016
|
df = pd.read_parquet(BytesIO(response.content))
|
|
902
|
-
|
|
1017
|
+
|
|
1018
|
+
return self._postprocess_dataframe(df)
|
|
903
1019
|
|
|
904
1020
|
def get_longitudinal_mean_max(
|
|
905
1021
|
self,
|
|
@@ -1359,6 +1475,7 @@ _generate_singleton_methods(
|
|
|
1359
1475
|
|
|
1360
1476
|
"get_sports",
|
|
1361
1477
|
"get_tags",
|
|
1478
|
+
"clear_cache",
|
|
1362
1479
|
|
|
1363
1480
|
"switch_user",
|
|
1364
1481
|
"switch_back",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.47.0 → sweatstack-0.48.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
{sweatstack-0.47.0 → sweatstack-0.48.0}/playground/Sweat Stack examples/Getting started.ipynb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sweatstack-0.47.0 → sweatstack-0.48.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
|