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.
- {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/settings.local.json +5 -1
- {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/client.md +5 -3
- sweatstack-0.72.0/.gitignore +45 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/CHANGELOG.md +16 -0
- sweatstack-0.72.0/CONTRIBUTING.md +9 -0
- sweatstack-0.72.0/LICENSE +21 -0
- sweatstack-0.72.0/PKG-INFO +46 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/README.md +1 -2
- {sweatstack-0.71.0 → sweatstack-0.72.0}/examples/fastapi_webhooks_example.py +10 -7
- {sweatstack-0.71.0 → sweatstack-0.72.0}/pyproject.toml +28 -7
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/client.py +61 -38
- {sweatstack-0.71.0 → sweatstack-0.72.0}/tests/test_webhooks.py +3 -3
- {sweatstack-0.71.0 → sweatstack-0.72.0}/uv.lock +1 -1
- sweatstack-0.71.0/.gitignore +0 -17
- sweatstack-0.71.0/CLIENT_DTYPE_CONVERSION.md +0 -808
- sweatstack-0.71.0/CLIENT_LIBRARY_SKILL.md +0 -156
- sweatstack-0.71.0/FASTAPI_DOCS.md +0 -275
- sweatstack-0.71.0/FASTAPI_PLUGIN.md +0 -396
- sweatstack-0.71.0/FASTAPI_USER_SWITCHING.md +0 -858
- sweatstack-0.71.0/FASTAPI_WEBHOOKS.md +0 -1466
- sweatstack-0.71.0/LOCAL_AUTH.md +0 -299
- sweatstack-0.71.0/PKG-INFO +0 -29
- sweatstack-0.71.0/examples/tokens.db +0 -0
- sweatstack-0.71.0/fastapi_coaching_example.py +0 -97
- sweatstack-0.71.0/fastapi_example.py +0 -95
- sweatstack-0.71.0/fastapi_sweatstack.py +0 -66
- sweatstack-0.71.0/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -262
- sweatstack-0.71.0/playground/README.md +0 -0
- sweatstack-0.71.0/playground/Untitled.ipynb +0 -298
- sweatstack-0.71.0/playground/hello.py +0 -6
- sweatstack-0.71.0/playground/pyproject.toml +0 -12
- sweatstack-0.71.0/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -351
- {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/SKILL.md +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/data-models.md +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/fastapi.md +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/.claude/skills/sweatstack-python/streamlit.md +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/.python-version +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/DEVELOPMENT.md +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/Makefile +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/docs/conf.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/docs/everything.rst +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/docs/index.rst +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/examples/send_webhook.py +0 -0
- {sweatstack-0.71.0/playground → sweatstack-0.72.0/src/sweatstack}/Sweat Stack examples/Getting started.ipynb +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/__init__.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/cli.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/constants.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/__init__.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/config.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/dependencies.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/models.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/routes.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/session.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/token_stores.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/fastapi/webhooks.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/ipython_init.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/openapi_schemas.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/py.typed +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/schemas.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/streamlit.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/sweatshell.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/src/sweatstack/utils.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/tests/__init__.py +0 -0
- {sweatstack-0.71.0 → sweatstack-0.72.0}/tests/test_dtype_conversion.py +0 -0
|
@@ -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
|
|
137
|
-
|
|
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="
|
|
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.
|
|
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@
|
|
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
|
|
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
|
|
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
|
-
|
|
100
|
+
cache_dir = str(self._get_cache_dir())
|
|
94
101
|
except Exception:
|
|
95
|
-
|
|
102
|
+
cache_dir = "unknown"
|
|
96
103
|
|
|
97
104
|
logging.warning(
|
|
98
|
-
f"Failed to {operation} cache
|
|
99
|
-
f"Cache directory: {
|
|
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
|
|
122
|
-
cache_dir = Path(
|
|
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(
|
|
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
|
|
130
|
-
"""Generate cache key for
|
|
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"
|
|
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
|
|
151
|
-
"""Try to read cached
|
|
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"
|
|
161
|
+
cache_file = cache_dir / f"{namespace}-{cache_key}.parquet"
|
|
155
162
|
|
|
156
163
|
if cache_file.exists():
|
|
157
|
-
return
|
|
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
|
|
164
|
-
"""Write
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1392
|
-
|
|
1393
|
-
if
|
|
1394
|
-
return self._postprocess_dataframe(
|
|
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.
|
|
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="
|
|
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", "
|
|
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="
|
|
482
|
+
session_secret="dGVzdC1vbmx5LWtleS1mb3ItdW5pdC10ZXN0cy0zMmI=",
|
|
483
483
|
token_store=store,
|
|
484
484
|
)
|
|
485
485
|
|
sweatstack-0.71.0/.gitignore
DELETED
|
@@ -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/
|