sweatstack 0.71.0__tar.gz → 0.72.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 (65) hide show
  1. {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/settings.local.json +5 -1
  2. {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/client.md +5 -3
  3. sweatstack-0.72.0/.gitignore +45 -0
  4. {sweatstack-0.71.0 → sweatstack-0.72.0}/CHANGELOG.md +16 -0
  5. sweatstack-0.72.0/CONTRIBUTING.md +9 -0
  6. sweatstack-0.72.0/LICENSE +21 -0
  7. sweatstack-0.72.0/PKG-INFO +46 -0
  8. {sweatstack-0.71.0 → sweatstack-0.72.0}/README.md +1 -2
  9. {sweatstack-0.71.0 → sweatstack-0.72.0}/examples/fastapi_webhooks_example.py +10 -7
  10. {sweatstack-0.71.0 → sweatstack-0.72.0}/pyproject.toml +28 -7
  11. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/client.py +61 -38
  12. {sweatstack-0.71.0 → sweatstack-0.72.0}/tests/test_webhooks.py +3 -3
  13. {sweatstack-0.71.0 → sweatstack-0.72.0}/uv.lock +1 -1
  14. sweatstack-0.71.0/.gitignore +0 -17
  15. sweatstack-0.71.0/CLIENT_DTYPE_CONVERSION.md +0 -808
  16. sweatstack-0.71.0/CLIENT_LIBRARY_SKILL.md +0 -156
  17. sweatstack-0.71.0/FASTAPI_DOCS.md +0 -275
  18. sweatstack-0.71.0/FASTAPI_PLUGIN.md +0 -396
  19. sweatstack-0.71.0/FASTAPI_USER_SWITCHING.md +0 -858
  20. sweatstack-0.71.0/FASTAPI_WEBHOOKS.md +0 -1466
  21. sweatstack-0.71.0/LOCAL_AUTH.md +0 -299
  22. sweatstack-0.71.0/PKG-INFO +0 -29
  23. sweatstack-0.71.0/examples/tokens.db +0 -0
  24. sweatstack-0.71.0/fastapi_coaching_example.py +0 -97
  25. sweatstack-0.71.0/fastapi_example.py +0 -95
  26. sweatstack-0.71.0/fastapi_sweatstack.py +0 -66
  27. sweatstack-0.71.0/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -262
  28. sweatstack-0.71.0/playground/README.md +0 -0
  29. sweatstack-0.71.0/playground/Untitled.ipynb +0 -298
  30. sweatstack-0.71.0/playground/hello.py +0 -6
  31. sweatstack-0.71.0/playground/pyproject.toml +0 -12
  32. sweatstack-0.71.0/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -351
  33. {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
  34. {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
  35. {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
  36. {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
  37. {sweatstack-0.71.0 → sweatstack-0.72.0}/.python-version +0 -0
  38. {sweatstack-0.71.0 → sweatstack-0.72.0}/DEVELOPMENT.md +0 -0
  39. {sweatstack-0.71.0 → sweatstack-0.72.0}/Makefile +0 -0
  40. {sweatstack-0.71.0 → sweatstack-0.72.0}/docs/conf.py +0 -0
  41. {sweatstack-0.71.0 → sweatstack-0.72.0}/docs/everything.rst +0 -0
  42. {sweatstack-0.71.0 → sweatstack-0.72.0}/docs/index.rst +0 -0
  43. {sweatstack-0.71.0 → sweatstack-0.72.0}/examples/send_webhook.py +0 -0
  44. {sweatstack-0.71.0/playground → sweatstack-0.72.0/src/sweatstack}/Sweat Stack examples/Getting started.ipynb +0 -0
  45. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/__init__.py +0 -0
  46. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/cli.py +0 -0
  47. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/constants.py +0 -0
  48. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/__init__.py +0 -0
  49. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/config.py +0 -0
  50. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/dependencies.py +0 -0
  51. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/models.py +0 -0
  52. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/routes.py +0 -0
  53. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/session.py +0 -0
  54. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/token_stores.py +0 -0
  55. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/webhooks.py +0 -0
  56. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/ipython_init.py +0 -0
  57. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  58. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/openapi_schemas.py +0 -0
  59. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/py.typed +0 -0
  60. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/schemas.py +0 -0
  61. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/streamlit.py +0 -0
  62. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/sweatshell.py +0 -0
  63. {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/utils.py +0 -0
  64. {sweatstack-0.71.0 → sweatstack-0.72.0}/tests/__init__.py +0 -0
  65. {sweatstack-0.71.0 → sweatstack-0.72.0}/tests/test_dtype_conversion.py +0 -0
@@ -14,7 +14,11 @@
14
14
  "Bash(uv run ruff:*)",
15
15
  "Bash(chmod:*)",
16
16
  "mcp__acp__Bash",
17
- "Bash(wc:*)"
17
+ "Bash(wc:*)",
18
+ "Bash(ls:*)",
19
+ "Bash(git:*)",
20
+ "Bash(python3:*)",
21
+ "Bash(test:*)"
18
22
  ],
19
23
  "deny": []
20
24
  }
@@ -131,12 +131,14 @@ df = client.get_longitudinal_awd(
131
131
 
132
132
  The DataFrame has a timezone-aware datetime index and includes an `activity_id` column — group by it for per-activity aggregation.
133
133
 
134
- **Local caching** for reproducible analysis (avoids re-fetching on reruns):
134
+ **Local caching** for reproducible analysis (avoids re-fetching on reruns). Caches `get_longitudinal_data()` and `get_longitudinal_mean_max()`:
135
135
  ```python
136
- import os
137
- os.environ["SWEATSTACK_LOCAL_CACHE"] = "true"
136
+ import sweatstack
137
+ sweatstack.enable_cache() # platform cache dir
138
+ sweatstack.enable_cache(path="./my_cache") # custom dir
138
139
  # Use fixed end dates (not "today") to get stable cache hits
139
140
  df = client.get_longitudinal_data(sports=[Sport.cycling], start=date(2025, 1, 1), end=date(2025, 3, 31))
141
+ df = client.get_longitudinal_mean_max(sports=[Sport.cycling], metric="power", start=date(2025, 1, 1))
140
142
  ```
141
143
 
142
144
  ## Traces
@@ -0,0 +1,45 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Environment variables and secrets
13
+ .env
14
+ .env.*
15
+
16
+ # Databases and local storage
17
+ *.db
18
+ *.sqlite
19
+ *.sqlite3
20
+
21
+ # IDE and editor configuration
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ *.swo
26
+ *~
27
+
28
+ # OS files
29
+ .DS_Store
30
+
31
+ # Test artifacts
32
+ .pytest_cache/
33
+
34
+ # Documentation
35
+ docs/_build/
36
+
37
+ # Internal docs dump
38
+ dump/
39
+
40
+ # Playground
41
+ playground/
42
+
43
+ # Claude skills (except our own)
44
+ .claude/skills/*
45
+ !.claude/skills/sweatstack-python/
@@ -6,6 +6,22 @@ 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.72.0] - 2026-03-13
10
+
11
+ ### Added
12
+ - `sweatstack.enable_cache()` function to enable local caching via a simple API call instead of environment variables.
13
+ - Local caching for `get_longitudinal_mean_max()` responses (previously only `get_longitudinal_data()` was cached).
14
+
15
+ ### Changed
16
+ - Cache directory now defaults to the platform cache dir (`platformdirs.user_cache_dir`) instead of the system temp directory.
17
+
18
+ ### Fixed
19
+ - `AttributeError` on Python <3.11 when the API returns an error response (`add_note` is a Python 3.11+ feature).
20
+
21
+ ### Removed
22
+ - `SWEATSTACK_LOCAL_CACHE` and `SWEATSTACK_CACHE_DIR` environment variables. Use `sweatstack.enable_cache()` instead.
23
+
24
+
9
25
  ## [0.71.0] - 2026-03-13
10
26
 
11
27
  ### Added
@@ -0,0 +1,9 @@
1
+ # Contributing
2
+
3
+ Thank you for your interest in the SweatStack Python client library!
4
+
5
+ This repository is open-sourced primarily to make it easier for developers and AI coding agents to understand and work with the library. At this time, we are **not accepting external contributions** such as pull requests or feature requests.
6
+
7
+ If you've found a bug or have a question about using the library, feel free to [open an issue](https://github.com/SweatStack/sweatstack-python/issues).
8
+
9
+ For general questions about SweatStack, please refer to the [developer documentation](https://developer.sweatstack.no/getting-started/).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Aart Goossens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: sweatstack
3
+ Version: 0.72.0
4
+ Summary: The official Python client for SweatStack
5
+ Project-URL: Homepage, https://sweatstack.no
6
+ Project-URL: Documentation, https://developer.sweatstack.no/getting-started/
7
+ Project-URL: Repository, https://github.com/SweatStack/sweatstack-python
8
+ Project-URL: Issues, https://github.com/SweatStack/sweatstack-python/issues
9
+ Project-URL: Changelog, https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md
10
+ Author-email: Aart Goossens <aart@sweatstack.no>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,client,fitness,sports,sweatstack
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: email-validator>=2.2.0
26
+ Requires-Dist: httpx>=0.28.1
27
+ Requires-Dist: pandas>=2.2.3
28
+ Requires-Dist: platformdirs>=4.0.0
29
+ Requires-Dist: pyarrow>=18.0.0
30
+ Requires-Dist: pydantic>=2.10.5
31
+ Provides-Extra: fastapi
32
+ Requires-Dist: cryptography>=41.0.0; extra == 'fastapi'
33
+ Requires-Dist: fastapi[standard]>=0.100.0; extra == 'fastapi'
34
+ Provides-Extra: jupyter
35
+ Requires-Dist: ipython>=8.31.0; extra == 'jupyter'
36
+ Requires-Dist: jupyterlab>=4.3.4; extra == 'jupyter'
37
+ Requires-Dist: matplotlib>=3.10.0; extra == 'jupyter'
38
+ Provides-Extra: streamlit
39
+ Requires-Dist: streamlit>=1.42.0; extra == 'streamlit'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # SweatStack Python client library
43
+
44
+ This is the official Python client library for SweatStack.
45
+
46
+ Documentation can be found [here](https://developer.sweatstack.no/getting-started/).
@@ -1,6 +1,5 @@
1
1
  # SweatStack Python client library
2
2
 
3
-
4
3
  This is the official Python client library for SweatStack.
5
4
 
6
- Documentation can be found [here](https://developer.sweatstack.no/getting-started/).
5
+ Documentation can be found [here](https://developer.sweatstack.no/getting-started/).
@@ -9,6 +9,14 @@ import logging
9
9
  import os
10
10
  from pathlib import Path
11
11
 
12
+ # Required environment variables:
13
+ # SWEATSTACK_CLIENT_ID
14
+ # SWEATSTACK_CLIENT_SECRET
15
+ # SWEATSTACK_SESSION_SECRET
16
+ # SWEATSTACK_WEBHOOK_SECRET
17
+ # SWEATSTACK_ENCRYPTION_KEY (for EncryptedSQLiteTokenStore)
18
+ # APP_URL (e.g. http://localhost:8001)
19
+
12
20
  from fastapi import FastAPI, Request
13
21
  from fastapi.responses import HTMLResponse, JSONResponse
14
22
 
@@ -28,18 +36,13 @@ logger = logging.getLogger(__name__)
28
36
 
29
37
  db_path = Path(__file__).parent / "tokens.db"
30
38
  token_store = EncryptedSQLiteTokenStore(
31
- encryption_key="8BIvhwKdURGpXCAGcfWm3Aw1gXNmqc3zWJGfnDCXVs4=",
39
+ encryption_key=os.environ["SWEATSTACK_ENCRYPTION_KEY"],
32
40
  db_path=db_path,
33
41
  )
34
42
 
35
43
  configure(
36
- client_id="1da141eeffa54c4d",
37
- client_secret="vgwMFP6uTQa3K5bBc1mDqU33xnHVwhyqVq-NLacopD0",
38
- session_secret="5YviwYhzDdmzsqbcEIP0QXzoiehIy8lqBWLtzaeBU8Q=",
39
- app_url="http://localhost:8001",
40
- webhook_secret="whsec_development_secret_key",
41
44
  token_store=token_store,
42
- )
45
+ ) # Uses SWEATSTACK_* environment variables (CLIENT_ID, CLIENT_SECRET, SESSION_SECRET, WEBHOOK_SECRET, APP_URL)
43
46
 
44
47
  app = FastAPI(title="SweatStack Webhooks Example")
45
48
  instrument(app)
@@ -1,12 +1,26 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.71.0"
3
+ version = "0.72.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
+ license = "MIT"
6
7
  authors = [
7
- { name = "Aart Goossens", email = "aart@gssns.io" }
8
+ { name = "Aart Goossens", email = "aart@sweatstack.no" }
8
9
  ]
9
10
  requires-python = ">=3.9"
11
+ keywords = ["sweatstack", "sports", "fitness", "api", "client"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ ]
10
24
  dependencies = [
11
25
  "email-validator>=2.2.0",
12
26
  "httpx>=0.28.1",
@@ -16,6 +30,13 @@ dependencies = [
16
30
  "pydantic>=2.10.5",
17
31
  ]
18
32
 
33
+ [project.urls]
34
+ Homepage = "https://sweatstack.no"
35
+ Documentation = "https://developer.sweatstack.no/getting-started/"
36
+ Repository = "https://github.com/SweatStack/sweatstack-python"
37
+ Issues = "https://github.com/SweatStack/sweatstack-python/issues"
38
+ Changelog = "https://github.com/SweatStack/sweatstack-python/blob/main/CHANGELOG.md"
39
+
19
40
  [project.optional-dependencies]
20
41
  streamlit = [
21
42
  "streamlit>=1.42.0",
@@ -30,6 +51,11 @@ fastapi = [
30
51
  "cryptography>=41.0.0",
31
52
  ]
32
53
 
54
+ [project.scripts]
55
+ generate-response-models = "sweatstack.cli:generate_response_models"
56
+ sweatshell = "sweatstack.sweatshell:run_ipython"
57
+ sweatlab = "sweatstack.jupyterlab_oauth2_startup:start_jupyterlab_with_oauth"
58
+
33
59
  [build-system]
34
60
  requires = ["hatchling"]
35
61
  build-backend = "hatchling.build"
@@ -45,8 +71,3 @@ dev = [
45
71
 
46
72
  [tool.uv.workspace]
47
73
  members = ["playground"]
48
-
49
- [project.scripts]
50
- generate-response-models = "sweatstack.cli:generate_response_models"
51
- sweatshell = "sweatstack.sweatshell:run_ipython"
52
- sweatlab = "sweatstack.jupyterlab_oauth2_startup:start_jupyterlab_with_oauth"
@@ -7,7 +7,6 @@ import logging
7
7
  import os
8
8
  import secrets
9
9
  import shutil
10
- import tempfile
11
10
  import time
12
11
  import urllib
13
12
  import warnings
@@ -26,7 +25,7 @@ from urllib.parse import parse_qs, urlparse
26
25
 
27
26
  import httpx
28
27
  import pandas as pd
29
- from platformdirs import user_data_dir
28
+ from platformdirs import user_cache_dir, user_data_dir
30
29
 
31
30
  from .constants import DEFAULT_URL
32
31
  from .schemas import (
@@ -40,6 +39,19 @@ logger = logging.getLogger(__name__)
40
39
  # Refresh tokens this many seconds before they expire to avoid race conditions
41
40
  TOKEN_EXPIRY_MARGIN_SECONDS = 5
42
41
 
42
+ # Module-level cache configuration. None = caching disabled.
43
+ _cache_config: dict | None = None
44
+
45
+
46
+ def enable_cache(path: str | None = None) -> None:
47
+ """Enable local caching of API responses.
48
+
49
+ Args:
50
+ path: Optional custom cache directory. Defaults to the platform cache dir.
51
+ """
52
+ global _cache_config
53
+ _cache_config = {"path": path}
54
+
43
55
 
44
56
  class TokenRefreshError(Exception):
45
57
  """Raised when automatic token refresh fails.
@@ -74,29 +86,24 @@ OAUTH2_CLIENT_ID = "5382f68b0d254378"
74
86
  class _LocalCacheMixin:
75
87
  """Mixin for handling local filesystem caching of API responses.
76
88
 
77
- Caching is controlled via environment variables:
78
-
79
- - :envvar:`SWEATSTACK_LOCAL_CACHE` - Enable/disable caching
80
- - :envvar:`SWEATSTACK_CACHE_DIR` - Custom cache directory location
81
-
89
+ Caching is enabled by calling :func:`sweatstack.enable_cache`.
82
90
  Use :meth:`clear_cache` to remove all cached data for the current user.
83
91
  """
84
92
 
85
93
  def _cache_enabled(self) -> bool:
86
94
  """Check if local caching is enabled."""
87
- return bool(os.getenv("SWEATSTACK_LOCAL_CACHE"))
95
+ return _cache_config is not None
88
96
 
89
97
  def _log_cache_error(self, operation: str, error: Exception) -> None:
90
98
  """Log cache operation errors with context."""
91
- cache_location = os.getenv("SWEATSTACK_CACHE_DIR") or tempfile.gettempdir()
92
99
  try:
93
- user_id = self._get_user_id_from_token()
100
+ cache_dir = str(self._get_cache_dir())
94
101
  except Exception:
95
- user_id = "unknown"
102
+ cache_dir = "unknown"
96
103
 
97
104
  logging.warning(
98
- f"Failed to {operation} cache despite SWEATSTACK_LOCAL_CACHE being enabled. "
99
- f"Cache directory: {cache_location}/sweatstack/{user_id}. "
105
+ f"Failed to {operation} cache. "
106
+ f"Cache directory: {cache_dir}. "
100
107
  f"Error: {error}"
101
108
  )
102
109
 
@@ -118,16 +125,16 @@ class _LocalCacheMixin:
118
125
  """Get cache directory for current user."""
119
126
  user_id = self._get_user_id_from_token()
120
127
 
121
- if cache_location := os.getenv("SWEATSTACK_CACHE_DIR"):
122
- cache_dir = Path(cache_location) / user_id
128
+ if _cache_config and _cache_config.get("path"):
129
+ cache_dir = Path(_cache_config["path"]) / user_id
123
130
  else:
124
- cache_dir = Path(tempfile.gettempdir()) / "sweatstack" / user_id
131
+ cache_dir = Path(user_cache_dir("SweatStack", "SweatStack")) / user_id
125
132
 
126
133
  cache_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
127
134
  return cache_dir
128
135
 
129
- def _generate_longitudinal_cache_key(self, **params) -> str:
130
- """Generate cache key for longitudinal data requests."""
136
+ def _generate_cache_key(self, namespace: str, **params) -> str:
137
+ """Generate a cache key for the given namespace and parameters."""
131
138
  normalized_params = {}
132
139
 
133
140
  for key, value in params.items():
@@ -144,37 +151,33 @@ class _LocalCacheMixin:
144
151
  else:
145
152
  normalized_params[key] = str(value)
146
153
 
147
- cache_data = f"longitudinal_data:{json.dumps(normalized_params, sort_keys=True)}"
154
+ cache_data = f"{namespace}:{json.dumps(normalized_params, sort_keys=True)}"
148
155
  return hashlib.sha256(cache_data.encode()).hexdigest()[:16]
149
156
 
150
- def _read_longitudinal_cache(self, cache_key: str) -> pd.DataFrame | None:
151
- """Try to read cached longitudinal data."""
157
+ def _read_cache(self, namespace: str, cache_key: str) -> bytes | None:
158
+ """Try to read cached data. Returns raw bytes or None."""
152
159
  try:
153
160
  cache_dir = self._get_cache_dir()
154
- cache_file = cache_dir / f"longitudinal-{cache_key}.parquet"
161
+ cache_file = cache_dir / f"{namespace}-{cache_key}.parquet"
155
162
 
156
163
  if cache_file.exists():
157
- return pd.read_parquet(cache_file)
164
+ return cache_file.read_bytes()
158
165
  except Exception as e:
159
166
  self._log_cache_error("read", e)
160
167
 
161
168
  return None
162
169
 
163
- def _write_longitudinal_cache(self, cache_key: str, content: bytes) -> None:
164
- """Write longitudinal data to cache."""
170
+ def _write_cache(self, namespace: str, cache_key: str, content: bytes) -> None:
171
+ """Write raw bytes to cache."""
165
172
  try:
166
173
  cache_dir = self._get_cache_dir()
167
- cache_file = cache_dir / f"longitudinal-{cache_key}.parquet"
174
+ cache_file = cache_dir / f"{namespace}-{cache_key}.parquet"
168
175
  cache_file.write_bytes(content)
169
176
  except Exception as e:
170
177
  self._log_cache_error("write", e)
171
178
 
172
179
  def clear_cache(self) -> None:
173
- """Clear all cached data for the current user.
174
-
175
- This removes all cached data (longitudinal data, etc.) from the temporary
176
- directory for the currently authenticated user.
177
- """
180
+ """Clear all cached data for the current user."""
178
181
  try:
179
182
  cache_dir = self._get_cache_dir()
180
183
  if cache_dir.exists():
@@ -948,12 +951,23 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
948
951
  with httpx.Client(base_url=self.url, headers=headers, timeout=60) as client:
949
952
  yield client
950
953
 
954
+ @staticmethod
955
+ def _add_note(exception: Exception, note: str):
956
+ """Add a note to an exception, compatible with Python <3.11."""
957
+ if hasattr(exception, "add_note"):
958
+ exception.add_note(note)
959
+ else:
960
+ if not exception.args:
961
+ exception.args = (note,)
962
+ else:
963
+ exception.args = (f"{exception.args[0]}\n{note}",) + exception.args[1:]
964
+
951
965
  def _print_response_and_raise(self, response: httpx.Response):
952
966
  try:
953
967
  response.raise_for_status()
954
968
  except httpx.HTTPStatusError as exception:
955
969
  additional_info = response.text
956
- exception.add_note(additional_info)
970
+ self._add_note(exception, additional_info)
957
971
  raise exception
958
972
 
959
973
  def _raise_for_status(self, response: httpx.Response):
@@ -973,7 +987,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
973
987
  "\nStreamlit environment detected. Use StreamlitAuth.client instance.\n"
974
988
  "Docs: https://developer.sweatstack.no/learn/integrations/streamlit/"
975
989
  )
976
- exception.add_note(streamlit_error_message)
990
+ self._add_note(exception, streamlit_error_message)
977
991
  raise
978
992
 
979
993
  else:
@@ -1388,10 +1402,10 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1388
1402
  params["adaptive_sampling_on"] = self._enums_to_strings([adaptive_sampling_on])[0]
1389
1403
 
1390
1404
  if self._cache_enabled():
1391
- cache_key = self._generate_longitudinal_cache_key(**params)
1392
- cached_df = self._read_longitudinal_cache(cache_key)
1393
- if cached_df is not None:
1394
- return self._postprocess_dataframe(cached_df)
1405
+ cache_key = self._generate_cache_key("longitudinal_data", **params)
1406
+ cached = self._read_cache("longitudinal_data", cache_key)
1407
+ if cached is not None:
1408
+ return self._postprocess_dataframe(pd.read_parquet(BytesIO(cached)))
1395
1409
 
1396
1410
  with self._http_client() as client:
1397
1411
  response = client.get(
@@ -1401,7 +1415,7 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1401
1415
  self._raise_for_status(response)
1402
1416
 
1403
1417
  if self._cache_enabled():
1404
- self._write_longitudinal_cache(cache_key, response.content)
1418
+ self._write_cache("longitudinal_data", cache_key, response.content)
1405
1419
 
1406
1420
  df = pd.read_parquet(BytesIO(response.content))
1407
1421
 
@@ -1469,6 +1483,12 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1469
1483
  if window_days is not None:
1470
1484
  params["window_days"] = window_days
1471
1485
 
1486
+ if self._cache_enabled():
1487
+ cache_key = self._generate_cache_key("mean_max", **params)
1488
+ cached = self._read_cache("mean_max", cache_key)
1489
+ if cached is not None:
1490
+ return self._postprocess_dataframe(pd.read_parquet(BytesIO(cached)))
1491
+
1472
1492
  with self._http_client() as client:
1473
1493
  response = client.get(
1474
1494
  url="/api/v1/activities/longitudinal-mean-max",
@@ -1476,6 +1496,9 @@ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixi
1476
1496
  )
1477
1497
  self._raise_for_status(response)
1478
1498
 
1499
+ if self._cache_enabled():
1500
+ self._write_cache("mean_max", cache_key, response.content)
1501
+
1479
1502
  df = pd.read_parquet(BytesIO(response.content))
1480
1503
  return self._postprocess_dataframe(df)
1481
1504
 
@@ -52,7 +52,7 @@ def configured_app(webhook_secret: str) -> FastAPI:
52
52
  client_id="test_client_id",
53
53
  client_secret="test_client_secret",
54
54
  app_url="http://localhost:8000",
55
- session_secret="5YviwYhzDdmzsqbcEIP0QXzoiehIy8lqBWLtzaeBU8Q=",
55
+ session_secret="dGVzdC1vbmx5LWtleS1mb3ItdW5pdC10ZXN0cy0zMmI=",
56
56
  webhook_secret=webhook_secret,
57
57
  )
58
58
  app = FastAPI()
@@ -460,7 +460,7 @@ class TestWebhookConfiguration:
460
460
  monkeypatch.setenv("SWEATSTACK_CLIENT_ID", "test_id")
461
461
  monkeypatch.setenv("SWEATSTACK_CLIENT_SECRET", "test_secret")
462
462
  monkeypatch.setenv("APP_URL", "http://localhost:8000")
463
- monkeypatch.setenv("SWEATSTACK_SESSION_SECRET", "5YviwYhzDdmzsqbcEIP0QXzoiehIy8lqBWLtzaeBU8Q=")
463
+ monkeypatch.setenv("SWEATSTACK_SESSION_SECRET", "dGVzdC1vbmx5LWtleS1mb3ItdW5pdC10ZXN0cy0zMmI=")
464
464
  monkeypatch.setenv("SWEATSTACK_WEBHOOK_SECRET", "whsec_from_env")
465
465
 
466
466
  configure()
@@ -479,7 +479,7 @@ class TestWebhookConfiguration:
479
479
  client_id="test_id",
480
480
  client_secret="test_secret",
481
481
  app_url="http://localhost:8000",
482
- session_secret="5YviwYhzDdmzsqbcEIP0QXzoiehIy8lqBWLtzaeBU8Q=",
482
+ session_secret="dGVzdC1vbmx5LWtleS1mb3ItdW5pdC10ZXN0cy0zMmI=",
483
483
  token_store=store,
484
484
  )
485
485
 
@@ -2456,7 +2456,7 @@ wheels = [
2456
2456
 
2457
2457
  [[package]]
2458
2458
  name = "sweatstack"
2459
- version = "0.67.0"
2459
+ version = "0.71.0"
2460
2460
  source = { editable = "." }
2461
2461
  dependencies = [
2462
2462
  { name = "email-validator" },
@@ -1,17 +0,0 @@
1
- # Python-generated files
2
- __pycache__/
3
- *.py[oc]
4
- build/
5
- dist/
6
- wheels/
7
- *.egg-info
8
-
9
- # Virtual environments
10
- .venv
11
-
12
- # Documentation
13
- docs/_build/
14
-
15
- # Claude skills (except our own)
16
- .claude/skills/*
17
- !.claude/skills/sweatstack-python/