sweatstack 0.47.0__tar.gz → 0.49.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 (29) hide show
  1. sweatstack-0.49.0/.claude/settings.local.json +8 -0
  2. {sweatstack-0.47.0 → sweatstack-0.49.0}/CHANGELOG.md +14 -0
  3. {sweatstack-0.47.0 → sweatstack-0.49.0}/PKG-INFO +1 -1
  4. {sweatstack-0.47.0 → sweatstack-0.49.0}/pyproject.toml +1 -1
  5. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/client.py +126 -3
  6. {sweatstack-0.47.0 → sweatstack-0.49.0}/.gitignore +0 -0
  7. {sweatstack-0.47.0 → sweatstack-0.49.0}/.python-version +0 -0
  8. {sweatstack-0.47.0 → sweatstack-0.49.0}/DEVELOPMENT.md +0 -0
  9. {sweatstack-0.47.0 → sweatstack-0.49.0}/Makefile +0 -0
  10. {sweatstack-0.47.0 → sweatstack-0.49.0}/README.md +0 -0
  11. {sweatstack-0.47.0 → sweatstack-0.49.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  12. {sweatstack-0.47.0 → sweatstack-0.49.0}/playground/README.md +0 -0
  13. {sweatstack-0.47.0 → sweatstack-0.49.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  14. {sweatstack-0.47.0 → sweatstack-0.49.0}/playground/Untitled.ipynb +0 -0
  15. {sweatstack-0.47.0 → sweatstack-0.49.0}/playground/hello.py +0 -0
  16. {sweatstack-0.47.0 → sweatstack-0.49.0}/playground/pyproject.toml +0 -0
  17. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  18. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/__init__.py +0 -0
  19. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/cli.py +0 -0
  20. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/constants.py +0 -0
  21. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/ipython_init.py +0 -0
  22. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  23. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/openapi_schemas.py +0 -0
  24. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/py.typed +0 -0
  25. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/schemas.py +0 -0
  26. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/streamlit.py +0 -0
  27. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/sweatshell.py +0 -0
  28. {sweatstack-0.47.0 → sweatstack-0.49.0}/src/sweatstack/utils.py +0 -0
  29. {sweatstack-0.47.0 → sweatstack-0.49.0}/uv.lock +0 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python:*)"
5
+ ],
6
+ "deny": []
7
+ }
8
+ }
@@ -6,6 +6,20 @@ 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.49.0] - 2025-08-18
10
+
11
+ ### Added
12
+
13
+ - Added a new `metrics` parameter to the `ss.get_activity_data()` and `ss.get_latest_activity_data()` methods that allows for filtering the data by specific metrics.
14
+
15
+
16
+ ## [0.48.0] - 2025-08-12
17
+
18
+ ### Added
19
+
20
+ - 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()`.
21
+
22
+
9
23
  ## [0.47.0] - 2025-08-07
10
24
 
11
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.47.0
3
+ Version: 0.49.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.47.0"
3
+ version = "0.49.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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,
@@ -723,6 +829,7 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
723
829
  self,
724
830
  activity_id: str,
725
831
  adaptive_sampling_on: Literal["power", "speed"] | None = None,
832
+ metrics: list[Metric | str] | None = None,
726
833
  ) -> pd.DataFrame:
727
834
  """Gets the raw data for a specific activity.
728
835
 
@@ -733,6 +840,7 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
733
840
  activity_id: The unique identifier of the activity.
734
841
  adaptive_sampling_on: Optional parameter to apply adaptive sampling on
735
842
  either "power" or "speed" data. If None, no adaptive sampling is applied.
843
+ metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
736
844
 
737
845
  Returns:
738
846
  pd.DataFrame: A pandas DataFrame containing the activity's time-series data.
@@ -743,6 +851,8 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
743
851
  params = {}
744
852
  if adaptive_sampling_on is not None:
745
853
  params["adaptive_sampling_on"] = adaptive_sampling_on
854
+ if metrics is not None:
855
+ params["metrics"] = self._enums_to_strings(metrics)
746
856
 
747
857
  with self._http_client() as client:
748
858
  response = client.get(
@@ -794,6 +904,7 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
794
904
  self,
795
905
  sport: Sport | str | None = None,
796
906
  adaptive_sampling_on: Literal["power", "speed"] | None = None,
907
+ metrics: list[Metric | str] | None = None,
797
908
  ) -> pd.DataFrame:
798
909
  """Gets the data for the latest activity of a specific sport.
799
910
 
@@ -804,6 +915,7 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
804
915
  sport: Optional sport to filter by. Can be a Sport enum or string.
805
916
  adaptive_sampling_on: Optional metric to apply adaptive sampling for visualization.
806
917
  Can be either "power" or "speed". Defaults to None.
918
+ metrics: Optional list of metrics to include in the results. Can be a list of Metric enums or strings.
807
919
 
808
920
  Returns:
809
921
  pd.DataFrame: A pandas DataFrame containing the activity data.
@@ -812,7 +924,7 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
812
924
  HTTPStatusError: If the API request fails.
813
925
  """
814
926
  activity = self.get_latest_activity(sport=sport)
815
- return self.get_activity_data(activity.id, adaptive_sampling_on)
927
+ return self.get_activity_data(activity.id, adaptive_sampling_on, metrics=metrics)
816
928
 
817
929
  def get_latest_activity_mean_max(
818
930
  self,
@@ -891,6 +1003,12 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
891
1003
  if adaptive_sampling_on is not None:
892
1004
  params["adaptive_sampling_on"] = self._enums_to_strings([adaptive_sampling_on])[0]
893
1005
 
1006
+ if self._cache_enabled():
1007
+ cache_key = self._generate_longitudinal_cache_key(**params)
1008
+ cached_df = self._read_longitudinal_cache(cache_key)
1009
+ if cached_df is not None:
1010
+ return self._postprocess_dataframe(cached_df)
1011
+
894
1012
  with self._http_client() as client:
895
1013
  response = client.get(
896
1014
  url="/api/v1/activities/longitudinal-data",
@@ -898,8 +1016,12 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin):
898
1016
  )
899
1017
  self._raise_for_status(response)
900
1018
 
1019
+ if self._cache_enabled():
1020
+ self._write_longitudinal_cache(cache_key, response.content)
1021
+
901
1022
  df = pd.read_parquet(BytesIO(response.content))
902
- return self._postprocess_dataframe(df)
1023
+
1024
+ return self._postprocess_dataframe(df)
903
1025
 
904
1026
  def get_longitudinal_mean_max(
905
1027
  self,
@@ -1359,6 +1481,7 @@ _generate_singleton_methods(
1359
1481
 
1360
1482
  "get_sports",
1361
1483
  "get_tags",
1484
+ "clear_cache",
1362
1485
 
1363
1486
  "switch_user",
1364
1487
  "switch_back",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes