sweatstack 0.54.0__tar.gz → 0.55.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 (33) hide show
  1. {sweatstack-0.54.0 → sweatstack-0.55.0}/.claude/settings.local.json +3 -2
  2. {sweatstack-0.54.0 → sweatstack-0.55.0}/.gitignore +3 -0
  3. {sweatstack-0.54.0 → sweatstack-0.55.0}/CHANGELOG.md +7 -0
  4. sweatstack-0.55.0/Makefile +12 -0
  5. {sweatstack-0.54.0 → sweatstack-0.55.0}/PKG-INFO +1 -1
  6. sweatstack-0.55.0/docs/conf.py +26 -0
  7. sweatstack-0.55.0/docs/everything.rst +171 -0
  8. {sweatstack-0.54.0 → sweatstack-0.55.0}/pyproject.toml +5 -1
  9. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/client.py +134 -6
  10. sweatstack-0.55.0/src/sweatstack/py.typed +0 -0
  11. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/schemas.py +36 -13
  12. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/streamlit.py +104 -5
  13. {sweatstack-0.54.0 → sweatstack-0.55.0}/uv.lock +196 -7
  14. sweatstack-0.54.0/Makefile +0 -6
  15. {sweatstack-0.54.0 → sweatstack-0.55.0}/.python-version +0 -0
  16. {sweatstack-0.54.0 → sweatstack-0.55.0}/DEVELOPMENT.md +0 -0
  17. {sweatstack-0.54.0 → sweatstack-0.55.0}/README.md +0 -0
  18. /sweatstack-0.54.0/playground/README.md → /sweatstack-0.55.0/docs/index.rst +0 -0
  19. {sweatstack-0.54.0 → sweatstack-0.55.0}/playground/.ipynb_checkpoints/Untitled-checkpoint.ipynb +0 -0
  20. /sweatstack-0.54.0/src/sweatstack/py.typed → /sweatstack-0.55.0/playground/README.md +0 -0
  21. {sweatstack-0.54.0 → sweatstack-0.55.0}/playground/Sweat Stack examples/Getting started.ipynb +0 -0
  22. {sweatstack-0.54.0 → sweatstack-0.55.0}/playground/Untitled.ipynb +0 -0
  23. {sweatstack-0.54.0 → sweatstack-0.55.0}/playground/hello.py +0 -0
  24. {sweatstack-0.54.0 → sweatstack-0.55.0}/playground/pyproject.toml +0 -0
  25. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/Sweat Stack examples/Getting started.ipynb +0 -0
  26. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/__init__.py +0 -0
  27. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/cli.py +0 -0
  28. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/constants.py +0 -0
  29. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/ipython_init.py +0 -0
  30. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/jupyterlab_oauth2_startup.py +0 -0
  31. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/openapi_schemas.py +0 -0
  32. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/sweatshell.py +0 -0
  33. {sweatstack-0.54.0 → sweatstack-0.55.0}/src/sweatstack/utils.py +0 -0
@@ -2,8 +2,9 @@
2
2
  "permissions": {
3
3
  "allow": [
4
4
  "Bash(python:*)",
5
- "WebFetch(domain:app.sweatstack.no)"
5
+ "WebFetch(domain:app.sweatstack.no)",
6
+ "Bash(cat:*)"
6
7
  ],
7
8
  "deny": []
8
9
  }
9
- }
10
+ }
@@ -8,3 +8,6 @@ wheels/
8
8
 
9
9
  # Virtual environments
10
10
  .venv
11
+
12
+ # Documentation
13
+ docs/_build/
@@ -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.55.0] - 2025-10-24
10
+
11
+ ### Added
12
+ - Added a new `get_activity_awd()` method to the `ss.Client` class that allows for getting the accumulated work duration (AWD) data for a specific activity.
13
+ - Added a new `get_longitudinal_awd()` method to the `ss.Client` class that allows for getting the AWD data for a specific date range.
14
+
15
+
9
16
  ## [0.54.0] - 2025-09-11
10
17
 
11
18
  ### Added
@@ -0,0 +1,12 @@
1
+ .PHONY: build publish docs
2
+
3
+
4
+ build:
5
+ rm -rf dist
6
+ uvx --from build pyproject-build --installer uv
7
+
8
+ publish: build
9
+ uvx twine upload dist/*
10
+
11
+ docs:
12
+ uv run sphinx-build -b markdown docs docs/_build/markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack
3
- Version: 0.54.0
3
+ Version: 0.55.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
@@ -0,0 +1,26 @@
1
+ import os
2
+ import sys
3
+
4
+ # So autodoc can import your package
5
+ sys.path.insert(0, os.path.abspath("..")) # adjust if needed
6
+
7
+ extensions = [
8
+ "sphinx.ext.autodoc",
9
+ "sphinx.ext.autosummary",
10
+ "sphinx.ext.napoleon", # if you use Google/NumPy docstrings
11
+ "sphinx_markdown_builder", # <-- this is the important one
12
+ "myst_parser", # optional, for .md sources
13
+ ]
14
+
15
+ autosummary_generate = True
16
+
17
+ # Autodoc configuration
18
+ autodoc_default_options = {
19
+ 'private-members': False, # Don't document private members (starting with _)
20
+ 'special-members': False, # Don't document special members (like __init__ unless specified)
21
+ }
22
+
23
+ # Optional nice-to-haves for markdown builder:
24
+ markdown_anchor_sections = True # add anchors to each section/function/class
25
+ markdown_anchor_signatures = True # add anchors to signatures
26
+ markdown_bullet = "*"
@@ -0,0 +1,171 @@
1
+ Complete API Reference
2
+ ======================
3
+
4
+ Environment Variables
5
+ =====================
6
+
7
+ The SweatStack client library uses environment variables for configuration.
8
+ These variables provide defaults for authentication, caching, and Streamlit integration.
9
+
10
+ Authentication
11
+ --------------
12
+
13
+ .. envvar:: SWEATSTACK_API_KEY
14
+
15
+ API access token for authenticating with SweatStack.
16
+
17
+ Automatically loaded by the Client if not provided during initialization.
18
+ Falls back to persistent storage if not found in environment.
19
+
20
+ Example::
21
+
22
+ export SWEATSTACK_API_KEY="your_token_here"
23
+
24
+ .. envvar:: SWEATSTACK_REFRESH_TOKEN
25
+
26
+ Refresh token used for automatic token renewal.
27
+
28
+ Loaded from environment, instance, or persistent storage. Used by the Client
29
+ to automatically refresh expired access tokens.
30
+
31
+ .. envvar:: SWEATSTACK_URL
32
+
33
+ Custom SweatStack instance URL.
34
+
35
+ Override the default SweatStack API URL. Useful for testing or
36
+ connecting to self-hosted instances.
37
+
38
+ Default: ``https://app.sweatstack.no``
39
+
40
+ Caching
41
+ -------
42
+
43
+ .. envvar:: SWEATSTACK_LOCAL_CACHE
44
+
45
+ Enable local filesystem caching of API responses.
46
+
47
+ Set to any truthy value (``1``, ``true``, ``yes``) to enable caching
48
+ of longitudinal data requests. Cached data persists across sessions.
49
+ Use ``client.clear_cache()`` to remove cached data.
50
+
51
+ Default: Disabled
52
+
53
+ Example::
54
+
55
+ export SWEATSTACK_LOCAL_CACHE=1
56
+
57
+ .. envvar:: SWEATSTACK_CACHE_DIR
58
+
59
+ Custom directory location for cached data.
60
+
61
+ Specify where to store cached API responses. If not set, defaults to
62
+ the system temp directory with a ``sweatstack/{user_id}`` subdirectory.
63
+
64
+ Default: System temp directory (e.g., ``/tmp/sweatstack/{user_id}``)
65
+
66
+ Example::
67
+
68
+ export SWEATSTACK_CACHE_DIR=/path/to/cache
69
+
70
+ Streamlit OAuth2
71
+ ----------------
72
+
73
+ These environment variables are used by :class:`sweatstack.streamlit.StreamlitAuth`
74
+ for OAuth2 authentication in Streamlit applications.
75
+
76
+ .. envvar:: SWEATSTACK_CLIENT_ID
77
+
78
+ OAuth2 application client ID.
79
+
80
+ The client ID from your registered SweatStack OAuth2 application.
81
+ Required for Streamlit authentication if not provided to StreamlitAuth.
82
+
83
+ .. envvar:: SWEATSTACK_CLIENT_SECRET
84
+
85
+ OAuth2 application client secret.
86
+
87
+ The client secret from your registered SweatStack OAuth2 application.
88
+ Required for Streamlit authentication if not provided to StreamlitAuth.
89
+
90
+ .. envvar:: SWEATSTACK_SCOPES
91
+
92
+ Comma-separated list of OAuth2 scopes.
93
+
94
+ Specify which permissions to request during OAuth2 authorization.
95
+
96
+ Default: ``data:read,profile``
97
+
98
+ Example::
99
+
100
+ export SWEATSTACK_SCOPES="data:read,data:write,profile"
101
+
102
+ .. envvar:: SWEATSTACK_REDIRECT_URI
103
+
104
+ OAuth2 redirect URI.
105
+
106
+ The URI where users are redirected after OAuth2 authorization.
107
+ Must match a redirect URI registered in your OAuth2 application.
108
+
109
+ Example::
110
+
111
+ export SWEATSTACK_REDIRECT_URI="http://localhost:8501"
112
+
113
+ Modules
114
+ =======
115
+
116
+ sweatstack
117
+ ----------
118
+
119
+ .. automodule:: sweatstack
120
+ :members:
121
+ :undoc-members:
122
+ :show-inheritance:
123
+
124
+ sweatstack.client
125
+ -----------------
126
+
127
+ .. automodule:: sweatstack.client
128
+ :members:
129
+ :undoc-members:
130
+ :inherited-members:
131
+ :show-inheritance:
132
+ :exclude-members: _LocalCacheMixin, _TokenStorageMixin, _OAuth2Mixin, _DelegationMixin
133
+
134
+
135
+ sweatstack.streamlit
136
+ --------------------
137
+
138
+ .. automodule:: sweatstack.streamlit
139
+ :members:
140
+ :undoc-members:
141
+ :show-inheritance:
142
+
143
+ sweatstack.schemas
144
+ ------------------
145
+
146
+ .. automodule:: sweatstack.schemas
147
+ :members:
148
+ :undoc-members:
149
+ :show-inheritance:
150
+
151
+ Sport
152
+ ~~~~~
153
+
154
+ .. autoclass:: sweatstack.schemas.Sport
155
+ :members: root_sport, parent_sport, is_sub_sport_of, is_root_sport, display_name
156
+ :undoc-members:
157
+
158
+ Metric
159
+ ~~~~~~
160
+
161
+ .. autoclass:: sweatstack.schemas.Metric
162
+ :members: display_name
163
+ :undoc-members:
164
+
165
+ sweatstack.openapi_schemas
166
+ ------------------
167
+
168
+ .. automodule:: sweatstack.openapi_schemas
169
+ :members:
170
+ :undoc-members:
171
+ :show-inheritance:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack"
3
- version = "0.54.0"
3
+ version = "0.55.0"
4
4
  description = "The official Python client for SweatStack"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -33,6 +33,10 @@ build-backend = "hatchling.build"
33
33
  [dependency-groups]
34
34
  dev = [
35
35
  "datamodel-code-generator>=0.26.5",
36
+ "myst-parser>=4.0.1",
37
+ "sphinx>=8.2.3",
38
+ "sphinx-markdown-builder>=0.6.8",
39
+ "streamlit>=1.42.0",
36
40
  ]
37
41
 
38
42
  [tool.uv.workspace]
@@ -51,8 +51,16 @@ AUTH_SUCCESSFUL_RESPONSE = """<!DOCTYPE html>
51
51
  OAUTH2_CLIENT_ID = "5382f68b0d254378"
52
52
 
53
53
 
54
- class LocalCacheMixin:
55
- """Mixin for handling local filesystem caching of API responses."""
54
+ class _LocalCacheMixin:
55
+ """Mixin for handling local filesystem caching of API responses.
56
+
57
+ Caching is controlled via environment variables:
58
+
59
+ - :envvar:`SWEATSTACK_LOCAL_CACHE` - Enable/disable caching
60
+ - :envvar:`SWEATSTACK_CACHE_DIR` - Custom cache directory location
61
+
62
+ Use :meth:`clear_cache` to remove all cached data for the current user.
63
+ """
56
64
 
57
65
  def _cache_enabled(self) -> bool:
58
66
  """Check if local caching is enabled."""
@@ -155,7 +163,7 @@ class LocalCacheMixin:
155
163
  self._log_cache_error("clear", e)
156
164
 
157
165
 
158
- class TokenStorageMixin:
166
+ class _TokenStorageMixin:
159
167
  """Mixin for handling persistent token storage using platformdirs."""
160
168
 
161
169
  def _get_token_file_path(self) -> Path:
@@ -200,7 +208,9 @@ except ImportError:
200
208
  __version__ = "unknown"
201
209
 
202
210
 
203
- class OAuth2Mixin:
211
+ class _OAuth2Mixin:
212
+ """OAuth2 authentication methods for the Client class."""
213
+
204
214
  def generate_pkce_params(self) -> tuple[str, str]:
205
215
  """Generate PKCE parameters for OAuth2 authorization.
206
216
 
@@ -436,7 +446,9 @@ class OAuth2Mixin:
436
446
  self.login(persist_api_key=persist_api_key)
437
447
 
438
448
 
439
- class DelegationMixin:
449
+ class _DelegationMixin:
450
+ """User delegation methods for accessing data on behalf of other users."""
451
+
440
452
  def _validate_user(self, user: str | UserSummary):
441
453
  if isinstance(user, UserSummary):
442
454
  return user.id
@@ -639,7 +651,18 @@ class DelegationMixin:
639
651
  )
640
652
 
641
653
 
642
- class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
654
+ class Client(_OAuth2Mixin, _DelegationMixin, _TokenStorageMixin, _LocalCacheMixin):
655
+ """SweatStack API client for accessing activities, traces, and user data.
656
+
657
+ The Client handles authentication, API requests, and data retrieval from SweatStack.
658
+ You can initialize it with credentials or use authenticate()/login() for OAuth2.
659
+
660
+ Example:
661
+ client = Client()
662
+ client.authenticate()
663
+ activities = client.get_activities(limit=10)
664
+ """
665
+
643
666
  def __init__(
644
667
  self,
645
668
  api_key: str | None = None,
@@ -647,6 +670,14 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
647
670
  url: str | None = None,
648
671
  streamlit_compatible: bool = False,
649
672
  ):
673
+ """Initialize a SweatStack client.
674
+
675
+ Args:
676
+ api_key: Optional API access token. If not provided, will check environment or storage.
677
+ refresh_token: Optional refresh token for automatic token renewal.
678
+ url: Optional SweatStack instance URL. Defaults to production.
679
+ streamlit_compatible: Set to True when using in Streamlit apps.
680
+ """
650
681
  self.api_key = api_key
651
682
  self.refresh_token = refresh_token
652
683
  self.url = url
@@ -684,6 +715,11 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
684
715
 
685
716
  @property
686
717
  def api_key(self) -> str:
718
+ """The current API access token.
719
+
720
+ Automatically loads from instance, environment (SWEATSTACK_API_KEY),
721
+ or persistent storage. Refreshes expired tokens automatically.
722
+ """
687
723
  if self._api_key is not None:
688
724
  value = self._api_key
689
725
  elif value := os.getenv("SWEATSTACK_API_KEY"):
@@ -703,6 +739,10 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
703
739
 
704
740
  @property
705
741
  def refresh_token(self) -> str:
742
+ """The refresh token used for automatic token renewal.
743
+
744
+ Loads from instance, environment (SWEATSTACK_REFRESH_TOKEN), or persistent storage.
745
+ """
706
746
  if self._refresh_token is not None:
707
747
  return self._refresh_token
708
748
  elif value := os.getenv("SWEATSTACK_REFRESH_TOKEN"):
@@ -1042,6 +1082,41 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
1042
1082
  df = pd.read_parquet(BytesIO(response.content))
1043
1083
  return self._postprocess_dataframe(df)
1044
1084
 
1085
+ def get_activity_awd(
1086
+ self,
1087
+ activity_id: str,
1088
+ metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"] | None = None,
1089
+ ) -> pd.DataFrame:
1090
+ """Gets the accumulated work duration (AWD) for a specific activity.
1091
+
1092
+ This method retrieves accumulated work duration metrics for a specific activity.
1093
+ AWD represents the total duration spent at each intensity level by sorting
1094
+ activity data by intensity.
1095
+
1096
+ Args:
1097
+ activity_id: The unique identifier of the activity.
1098
+ metric: Optional metric type. Defaults to power for cycling, speed for other sports.
1099
+ Can be either "power" or "speed".
1100
+
1101
+ Returns:
1102
+ pd.DataFrame: A pandas DataFrame containing the AWD data.
1103
+
1104
+ Raises:
1105
+ HTTPStatusError: If the API request fails.
1106
+ """
1107
+ params = {}
1108
+ if metric is not None:
1109
+ params["metric"] = self._enums_to_strings([metric])[0]
1110
+
1111
+ with self._http_client() as client:
1112
+ response = client.get(
1113
+ url=f"/api/v1/activities/{activity_id}/accumulated-work-duration",
1114
+ params=params,
1115
+ )
1116
+ self._raise_for_status(response)
1117
+ df = pd.read_parquet(BytesIO(response.content))
1118
+ return self._postprocess_dataframe(df)
1119
+
1045
1120
  def get_latest_activity_data(
1046
1121
  self,
1047
1122
  sport: Sport | str | None = None,
@@ -1215,6 +1290,57 @@ class Client(OAuth2Mixin, DelegationMixin, TokenStorageMixin, LocalCacheMixin):
1215
1290
  df = pd.read_parquet(BytesIO(response.content))
1216
1291
  return self._postprocess_dataframe(df)
1217
1292
 
1293
+ def get_longitudinal_awd(
1294
+ self,
1295
+ *,
1296
+ sport: Sport | str,
1297
+ metric: Literal[Metric.power, Metric.speed] | Literal["power", "speed"],
1298
+ date: date | str | None = None,
1299
+ window_days: int | None = None,
1300
+ ) -> pd.DataFrame:
1301
+ """Gets the longitudinal accumulated work duration (AWD) for a specific sport and metric.
1302
+
1303
+ This method retrieves AWD values across four intensity levels: max (highest daily AWD),
1304
+ hard, medium, and easy (sustainable durations for respective workout intensities).
1305
+
1306
+ Note: This endpoint is in development and subject to change.
1307
+
1308
+ Args:
1309
+ sport: The sport to get AWD data for. Can be a Sport enum or string ID.
1310
+ metric: The metric to calculate AWD for. Must be either "power" or "speed".
1311
+ date: Optional reference date for the AWD calculation. If provided,
1312
+ the AWD will be calculated up to this date. Can be a date object
1313
+ or string in ISO format.
1314
+ window_days: Optional number of days to include in the calculation window
1315
+ before the reference date. If None, all available data is used.
1316
+
1317
+ Returns:
1318
+ pd.DataFrame: A pandas DataFrame containing the longitudinal AWD data with intensity levels.
1319
+
1320
+ Raises:
1321
+ HTTPStatusError: If the API request fails.
1322
+ """
1323
+ sport = self._enums_to_strings([sport])[0]
1324
+ metric = self._enums_to_strings([metric])[0]
1325
+
1326
+ params = {
1327
+ "sport": sport,
1328
+ "metric": metric,
1329
+ }
1330
+ if date is not None:
1331
+ params["date"] = date
1332
+ if window_days is not None:
1333
+ params["window_days"] = window_days
1334
+
1335
+ with self._http_client() as client:
1336
+ response = client.get(
1337
+ url="/api/v1/activities/longitudinal-accumulated-work-duration",
1338
+ params=params,
1339
+ )
1340
+ self._raise_for_status(response)
1341
+ df = pd.read_parquet(BytesIO(response.content))
1342
+ return self._postprocess_dataframe(df)
1343
+
1218
1344
  def _get_traces_generator(
1219
1345
  self,
1220
1346
  *,
@@ -1621,6 +1747,7 @@ _generate_singleton_methods(
1621
1747
  "get_activity",
1622
1748
  "get_activity_data",
1623
1749
  "get_activity_mean_max",
1750
+ "get_activity_awd",
1624
1751
 
1625
1752
  "get_latest_activity",
1626
1753
  "get_latest_activity_data",
@@ -1628,6 +1755,7 @@ _generate_singleton_methods(
1628
1755
 
1629
1756
  "get_longitudinal_data",
1630
1757
  "get_longitudinal_mean_max",
1758
+ "get_longitudinal_awd",
1631
1759
 
1632
1760
  "get_traces",
1633
1761
  "create_trace",
File without changes
@@ -1,3 +1,15 @@
1
+ """SweatStack data schemas and utilities.
2
+
3
+ This module re-exports Pydantic models from openapi_schemas and extends
4
+ the Sport and Metric enums with convenient utility methods.
5
+
6
+ Example:
7
+ sport = Sport.cycling_road
8
+ print(sport.display_name()) # "cycling (road)"
9
+ print(sport.root_sport()) # Sport.cycling
10
+ print(sport.is_root_sport()) # False
11
+ print(sport.is_sub_sport_of(Sport.cycling)) # True
12
+ """
1
13
  from enum import Enum
2
14
  from typing import List, Union
3
15
 
@@ -7,7 +19,7 @@ from .openapi_schemas import (
7
19
  )
8
20
 
9
21
 
10
- def parent_sport(sport: Sport) -> Sport:
22
+ def _parent_sport(sport: Sport) -> Sport:
11
23
  """Returns the parent sport of a given sport.
12
24
 
13
25
  For sports with a hierarchical structure (e.g., 'cycling.road'), returns the parent sport
@@ -25,7 +37,7 @@ def parent_sport(sport: Sport) -> Sport:
25
37
  return sport.__class__(".".join(parts[:-1]))
26
38
 
27
39
 
28
- def root_sport(sport: Sport) -> Sport:
40
+ def _root_sport(sport: Sport) -> Sport:
29
41
  """Returns the root sport of a given sport.
30
42
 
31
43
  For sports with a hierarchical structure (e.g., 'cycling.road' or 'cycling.road.gravel'),
@@ -40,7 +52,7 @@ def root_sport(sport: Sport) -> Sport:
40
52
  return sport.__class__(sport.value.split(".")[0])
41
53
 
42
54
 
43
- def is_root_sport(sport: Sport) -> bool:
55
+ def _is_root_sport(sport: Sport) -> bool:
44
56
  """Determines if a sport is a root sport.
45
57
 
46
58
  A root sport is one that doesn't have a parent sport in the hierarchy
@@ -52,10 +64,10 @@ def is_root_sport(sport: Sport) -> bool:
52
64
  Returns:
53
65
  bool: True if the sport is a root sport, False otherwise.
54
66
  """
55
- return sport == root_sport(sport)
67
+ return sport == _root_sport(sport)
56
68
 
57
69
 
58
- def is_sub_sport_of(sport: Sport, sport_or_sports: Union[Sport, List[Sport]]) -> bool:
70
+ def _is_sub_sport_of(sport: Sport, sport_or_sports: Union[Sport, List[Sport]]) -> bool:
59
71
  """Determines if a sport is a sub-sport of another sport or list of sports.
60
72
 
61
73
  For example, 'cycling.road' is a sub-sport of 'cycling', but not of 'running'.
@@ -73,12 +85,12 @@ def is_sub_sport_of(sport: Sport, sport_or_sports: Union[Sport, List[Sport]]) ->
73
85
  if isinstance(sport_or_sports, Sport):
74
86
  return sport.value.startswith(sport_or_sports.value)
75
87
  elif isinstance(sport_or_sports, (list, tuple)):
76
- return any(is_sub_sport_of(sport, sport) for sport in sport_or_sports)
88
+ return any(_is_sub_sport_of(sport, s) for s in sport_or_sports)
77
89
  else:
78
90
  raise ValueError(f"Invalid type for sport_or_sports: {type(sport_or_sports)}")
79
91
 
80
92
 
81
- def display_name(sport: Sport) -> str:
93
+ def _display_name(sport: Sport) -> str:
82
94
  """Returns a human-readable display name for a sport.
83
95
 
84
96
  This function converts a Sport enum value into a formatted string suitable for display.
@@ -98,13 +110,23 @@ def display_name(sport: Sport) -> str:
98
110
  return f"{base_sport} ({the_rest})"
99
111
 
100
112
 
101
- Sport.root_sport = root_sport
102
- Sport.parent_sport = parent_sport
103
- Sport.is_sub_sport_of = is_sub_sport_of
104
- Sport.display_name = display_name
113
+ Sport.root_sport = _root_sport
114
+ Sport.root_sport.__doc__ = _root_sport.__doc__
105
115
 
116
+ Sport.parent_sport = _parent_sport
117
+ Sport.parent_sport.__doc__ = _parent_sport.__doc__
106
118
 
107
- def metric_display_name(metric: Metric) -> str:
119
+ Sport.is_sub_sport_of = _is_sub_sport_of
120
+ Sport.is_sub_sport_of.__doc__ = _is_sub_sport_of.__doc__
121
+
122
+ Sport.is_root_sport = _is_root_sport
123
+ Sport.is_root_sport.__doc__ = _is_root_sport.__doc__
124
+
125
+ Sport.display_name = _display_name
126
+ Sport.display_name.__doc__ = _display_name.__doc__
127
+
128
+
129
+ def _metric_display_name(metric: Metric) -> str:
108
130
  """Returns a human-readable display name for a metric.
109
131
 
110
132
  This function converts a Metric enum value into a formatted string suitable for display.
@@ -112,4 +134,5 @@ def metric_display_name(metric: Metric) -> str:
112
134
  return metric.value.replace("_", " ")
113
135
 
114
136
 
115
- Metric.display_name = metric_display_name
137
+ Metric.display_name = _metric_display_name
138
+ Metric.display_name.__doc__ = _metric_display_name.__doc__
@@ -1,3 +1,29 @@
1
+ """Streamlit integration for SweatStack authentication and UI components.
2
+
3
+ This module provides authentication and UI helper components for building
4
+ Streamlit applications with SweatStack. The StreamlitAuth class handles
5
+ OAuth2 authentication flow and provides convenient selector components.
6
+
7
+ Example:
8
+ import streamlit as st
9
+ from sweatstack.streamlit import StreamlitAuth
10
+
11
+ auth = StreamlitAuth(
12
+ client_id="YOUR_APPLICATION_ID",
13
+ client_secret="YOUR_APPLICATION_SECRET",
14
+ redirect_uri="http://localhost:8501",
15
+ )
16
+
17
+ with st.sidebar:
18
+ auth.authenticate()
19
+
20
+ if not auth.is_authenticated():
21
+ st.stop()
22
+
23
+ st.write("Welcome!")
24
+ latest = auth.client.get_latest_activity()
25
+ st.write(f"Latest: {latest.sport}")
26
+ """
1
27
  import os
2
28
  import urllib.parse
3
29
  from datetime import date
@@ -18,6 +44,46 @@ from .schemas import Metric, Scope, Sport
18
44
 
19
45
 
20
46
  class StreamlitAuth:
47
+ """Handles SweatStack authentication and provides UI components for Streamlit apps.
48
+
49
+ This class manages OAuth2 authentication flow for Streamlit applications and provides
50
+ convenient selector components for activities, sports, tags, and metrics. Once authenticated,
51
+ the client property provides access to the SweatStack API.
52
+
53
+ Example:
54
+ import streamlit as st
55
+ from sweatstack.streamlit import StreamlitAuth
56
+
57
+ # Initialize authentication
58
+ auth = StreamlitAuth(
59
+ client_id="YOUR_APPLICATION_ID",
60
+ client_secret="YOUR_APPLICATION_SECRET",
61
+ redirect_uri="http://localhost:8501",
62
+ )
63
+
64
+ # Add authentication to sidebar
65
+ with st.sidebar:
66
+ auth.authenticate()
67
+
68
+ # Check authentication
69
+ if not auth.is_authenticated():
70
+ st.write("Please log in to continue")
71
+ st.stop()
72
+
73
+ # Use the authenticated client
74
+ st.write("Welcome to SweatStack")
75
+ latest_activity = auth.client.get_latest_activity()
76
+ st.write(f"Latest activity: {latest_activity.sport} on {latest_activity.start}")
77
+
78
+ # Switch between accessible users (admin feature)
79
+ with st.sidebar:
80
+ auth.select_user()
81
+
82
+ Attributes:
83
+ client: The SweatStack Client instance for API access.
84
+ api_key: The current API access token.
85
+ """
86
+
21
87
  def __init__(
22
88
  self,
23
89
  client_id=None,
@@ -25,12 +91,13 @@ class StreamlitAuth:
25
91
  scopes: List[Union[str, Scope]]=None,
26
92
  redirect_uri=None,
27
93
  ):
28
- """
94
+ """Initialize the StreamlitAuth component.
95
+
29
96
  Args:
30
- client_id: The client ID to use. If not provided, the SWEATSTACK_CLIENT_ID environment variable will be used.
31
- client_secret: The client secret to use. If not provided, the SWEATSTACK_CLIENT_SECRET environment variable will be used.
32
- scopes: The scopes to use. If not provided, the SWEATSTACK_SCOPES environment variable will be used. Defaults to data:read, profile.
33
- redirect_uri: The redirect URI to use. If not provided, the SWEATSTACK_REDIRECT_URI environment variable will be used.
97
+ client_id: OAuth2 client ID. Falls back to SWEATSTACK_CLIENT_ID env var.
98
+ client_secret: OAuth2 client secret. Falls back to SWEATSTACK_CLIENT_SECRET env var.
99
+ scopes: OAuth2 scopes. Falls back to SWEATSTACK_SCOPES env var. Defaults to data:read, profile.
100
+ redirect_uri: OAuth2 redirect URI. Falls back to SWEATSTACK_REDIRECT_URI env var.
34
101
  """
35
102
  self.client_id = client_id or os.environ.get("SWEATSTACK_CLIENT_ID")
36
103
  self.client_secret = client_secret or os.environ.get("SWEATSTACK_CLIENT_SECRET")
@@ -51,6 +118,11 @@ class StreamlitAuth:
51
118
  self.client = Client(self.api_key, streamlit_compatible=True)
52
119
 
53
120
  def logout_button(self):
121
+ """Displays a logout button and handles user logout.
122
+
123
+ When clicked, this button clears the stored API key from session state,
124
+ resets the client, and triggers a Streamlit rerun to update the UI.
125
+ """
54
126
  if st.button("Logout"):
55
127
  self.api_key = None
56
128
  self.client = Client(streamlit_compatible=True)
@@ -58,9 +130,15 @@ class StreamlitAuth:
58
130
  st.rerun()
59
131
 
60
132
  def _running_on_streamlit_cloud(self):
133
+ """Detects if the app is running on Streamlit Cloud."""
61
134
  return os.environ.get("HOSTNAME") == "streamlit"
62
135
 
63
136
  def _show_sweatstack_login(self, login_label: str | None = None):
137
+ """Displays the SweatStack login button with appropriate styling.
138
+
139
+ Args:
140
+ login_label: Text to display on the login button.
141
+ """
64
142
  authorization_url = self.get_authorization_url()
65
143
  login_label = login_label or "Connect with SweatStack"
66
144
  if not self._running_on_streamlit_cloud():
@@ -96,6 +174,14 @@ class StreamlitAuth:
96
174
  st.link_button(login_label, authorization_url)
97
175
 
98
176
  def get_authorization_url(self):
177
+ """Generates the OAuth2 authorization URL for SweatStack.
178
+
179
+ This method constructs the URL users will be redirected to for OAuth2 authorization.
180
+ It includes the client ID, redirect URI, scopes, and other OAuth2 parameters.
181
+
182
+ Returns:
183
+ str: The complete authorization URL.
184
+ """
99
185
  params = {
100
186
  "client_id": self.client_id,
101
187
  "redirect_uri": self.redirect_uri,
@@ -108,11 +194,24 @@ class StreamlitAuth:
108
194
  return authorization_url
109
195
 
110
196
  def _set_api_key(self, api_key):
197
+ """Sets the API key in instance and session state, then refreshes the client.
198
+
199
+ Args:
200
+ api_key: The API access token to set.
201
+ """
111
202
  self.api_key = api_key
112
203
  st.session_state["sweatstack_api_key"] = api_key
113
204
  self.client = Client(self.api_key, streamlit_compatible=True)
114
205
 
115
206
  def _exchange_token(self, code):
207
+ """Exchanges an authorization code for an access token.
208
+
209
+ Args:
210
+ code: The authorization code from the OAuth2 callback.
211
+
212
+ Raises:
213
+ Exception: If the token exchange fails.
214
+ """
116
215
  token_data = {
117
216
  "grant_type": "authorization_code",
118
217
  "client_id": self.client_id,
@@ -1,9 +1,9 @@
1
1
  version = 1
2
- revision = 2
2
+ revision = 3
3
3
  requires-python = ">=3.12"
4
4
  resolution-markers = [
5
- "python_full_version < '4.0'",
6
- "python_full_version >= '4.0'",
5
+ "python_full_version < '4'",
6
+ "python_full_version >= '4'",
7
7
  ]
8
8
 
9
9
  [manifest]
@@ -12,6 +12,15 @@ members = [
12
12
  "sweatstack",
13
13
  ]
14
14
 
15
+ [[package]]
16
+ name = "alabaster"
17
+ version = "1.0.0"
18
+ source = { registry = "https://pypi.org/simple" }
19
+ sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" }
20
+ wheels = [
21
+ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
22
+ ]
23
+
15
24
  [[package]]
16
25
  name = "altair"
17
26
  version = "5.5.0"
@@ -394,7 +403,7 @@ dependencies = [
394
403
  { name = "isort" },
395
404
  { name = "jinja2" },
396
405
  { name = "packaging" },
397
- { name = "pydantic", extra = ["email"], marker = "python_full_version < '4.0'" },
406
+ { name = "pydantic", extra = ["email"], marker = "python_full_version < '4'" },
398
407
  { name = "pyyaml" },
399
408
  ]
400
409
  sdist = { url = "https://files.pythonhosted.org/packages/63/e4/53153452235a387112df40f67aaf24072d4b5e33aa7bb385004f4c4baf38/datamodel_code_generator-0.26.5.tar.gz", hash = "sha256:c4a94a7dbf7972129882732d9bcee44c9ae090f57c82edd58d237b9d48c40dd0", size = 92586, upload-time = "2025-01-14T12:02:07.692Z" }
@@ -446,6 +455,15 @@ wheels = [
446
455
  { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" },
447
456
  ]
448
457
 
458
+ [[package]]
459
+ name = "docutils"
460
+ version = "0.21.2"
461
+ source = { registry = "https://pypi.org/simple" }
462
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" }
463
+ wheels = [
464
+ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
465
+ ]
466
+
449
467
  [[package]]
450
468
  name = "email-validator"
451
469
  version = "2.2.0"
@@ -590,6 +608,15 @@ wheels = [
590
608
  { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
591
609
  ]
592
610
 
611
+ [[package]]
612
+ name = "imagesize"
613
+ version = "1.4.1"
614
+ source = { registry = "https://pypi.org/simple" }
615
+ sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" }
616
+ wheels = [
617
+ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" },
618
+ ]
619
+
593
620
  [[package]]
594
621
  name = "inflect"
595
622
  version = "5.6.2"
@@ -1049,6 +1076,18 @@ wheels = [
1049
1076
  { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" },
1050
1077
  ]
1051
1078
 
1079
+ [[package]]
1080
+ name = "mdit-py-plugins"
1081
+ version = "0.5.0"
1082
+ source = { registry = "https://pypi.org/simple" }
1083
+ dependencies = [
1084
+ { name = "markdown-it-py" },
1085
+ ]
1086
+ sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" }
1087
+ wheels = [
1088
+ { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" },
1089
+ ]
1090
+
1052
1091
  [[package]]
1053
1092
  name = "mdurl"
1054
1093
  version = "0.1.2"
@@ -1076,6 +1115,23 @@ wheels = [
1076
1115
  { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" },
1077
1116
  ]
1078
1117
 
1118
+ [[package]]
1119
+ name = "myst-parser"
1120
+ version = "4.0.1"
1121
+ source = { registry = "https://pypi.org/simple" }
1122
+ dependencies = [
1123
+ { name = "docutils" },
1124
+ { name = "jinja2" },
1125
+ { name = "markdown-it-py" },
1126
+ { name = "mdit-py-plugins" },
1127
+ { name = "pyyaml" },
1128
+ { name = "sphinx" },
1129
+ ]
1130
+ sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" }
1131
+ wheels = [
1132
+ { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" },
1133
+ ]
1134
+
1079
1135
  [[package]]
1080
1136
  name = "narwhals"
1081
1137
  version = "1.26.0"
@@ -1469,7 +1525,7 @@ wheels = [
1469
1525
 
1470
1526
  [package.optional-dependencies]
1471
1527
  email = [
1472
- { name = "email-validator", marker = "python_full_version < '4.0'" },
1528
+ { name = "email-validator", marker = "python_full_version < '4'" },
1473
1529
  ]
1474
1530
 
1475
1531
  [[package]]
@@ -1728,6 +1784,15 @@ wheels = [
1728
1784
  { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" },
1729
1785
  ]
1730
1786
 
1787
+ [[package]]
1788
+ name = "roman-numerals-py"
1789
+ version = "3.1.0"
1790
+ source = { registry = "https://pypi.org/simple" }
1791
+ sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" }
1792
+ wheels = [
1793
+ { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" },
1794
+ ]
1795
+
1731
1796
  [[package]]
1732
1797
  name = "rpds-py"
1733
1798
  version = "0.22.3"
@@ -1820,6 +1885,15 @@ wheels = [
1820
1885
  { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
1821
1886
  ]
1822
1887
 
1888
+ [[package]]
1889
+ name = "snowballstemmer"
1890
+ version = "3.0.1"
1891
+ source = { registry = "https://pypi.org/simple" }
1892
+ sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" }
1893
+ wheels = [
1894
+ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" },
1895
+ ]
1896
+
1823
1897
  [[package]]
1824
1898
  name = "soupsieve"
1825
1899
  version = "2.6"
@@ -1829,6 +1903,102 @@ wheels = [
1829
1903
  { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186, upload-time = "2024-08-13T13:39:10.986Z" },
1830
1904
  ]
1831
1905
 
1906
+ [[package]]
1907
+ name = "sphinx"
1908
+ version = "8.2.3"
1909
+ source = { registry = "https://pypi.org/simple" }
1910
+ dependencies = [
1911
+ { name = "alabaster" },
1912
+ { name = "babel" },
1913
+ { name = "colorama", marker = "sys_platform == 'win32'" },
1914
+ { name = "docutils" },
1915
+ { name = "imagesize" },
1916
+ { name = "jinja2" },
1917
+ { name = "packaging" },
1918
+ { name = "pygments" },
1919
+ { name = "requests" },
1920
+ { name = "roman-numerals-py" },
1921
+ { name = "snowballstemmer" },
1922
+ { name = "sphinxcontrib-applehelp" },
1923
+ { name = "sphinxcontrib-devhelp" },
1924
+ { name = "sphinxcontrib-htmlhelp" },
1925
+ { name = "sphinxcontrib-jsmath" },
1926
+ { name = "sphinxcontrib-qthelp" },
1927
+ { name = "sphinxcontrib-serializinghtml" },
1928
+ ]
1929
+ sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" }
1930
+ wheels = [
1931
+ { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" },
1932
+ ]
1933
+
1934
+ [[package]]
1935
+ name = "sphinx-markdown-builder"
1936
+ version = "0.6.8"
1937
+ source = { registry = "https://pypi.org/simple" }
1938
+ dependencies = [
1939
+ { name = "docutils" },
1940
+ { name = "sphinx" },
1941
+ { name = "tabulate" },
1942
+ ]
1943
+ sdist = { url = "https://files.pythonhosted.org/packages/74/36/f4a2efb804e2b89a6a29338bd1e9895af806e465c4a13ca59271f9d40dfd/sphinx_markdown_builder-0.6.8.tar.gz", hash = "sha256:6141b566bf18dd1cd515a0a90efd91c6c4d10fc638554fab2fd19cba66543dd7", size = 22007, upload-time = "2025-01-19T01:58:20.497Z" }
1944
+ wheels = [
1945
+ { url = "https://files.pythonhosted.org/packages/31/98/7e8e11d4edce0947d89c5d00ed43d925a5254dc9733579382b04f77e5ff2/sphinx_markdown_builder-0.6.8-py3-none-any.whl", hash = "sha256:f04ab42d52449363228b9104569c56b778534f9c41a168af8cfc721a1e0e3edc", size = 17270, upload-time = "2025-01-19T01:58:19.296Z" },
1946
+ ]
1947
+
1948
+ [[package]]
1949
+ name = "sphinxcontrib-applehelp"
1950
+ version = "2.0.0"
1951
+ source = { registry = "https://pypi.org/simple" }
1952
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" }
1953
+ wheels = [
1954
+ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" },
1955
+ ]
1956
+
1957
+ [[package]]
1958
+ name = "sphinxcontrib-devhelp"
1959
+ version = "2.0.0"
1960
+ source = { registry = "https://pypi.org/simple" }
1961
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" }
1962
+ wheels = [
1963
+ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" },
1964
+ ]
1965
+
1966
+ [[package]]
1967
+ name = "sphinxcontrib-htmlhelp"
1968
+ version = "2.1.0"
1969
+ source = { registry = "https://pypi.org/simple" }
1970
+ sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" }
1971
+ wheels = [
1972
+ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" },
1973
+ ]
1974
+
1975
+ [[package]]
1976
+ name = "sphinxcontrib-jsmath"
1977
+ version = "1.0.1"
1978
+ source = { registry = "https://pypi.org/simple" }
1979
+ sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" }
1980
+ wheels = [
1981
+ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" },
1982
+ ]
1983
+
1984
+ [[package]]
1985
+ name = "sphinxcontrib-qthelp"
1986
+ version = "2.0.0"
1987
+ source = { registry = "https://pypi.org/simple" }
1988
+ sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" }
1989
+ wheels = [
1990
+ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" },
1991
+ ]
1992
+
1993
+ [[package]]
1994
+ name = "sphinxcontrib-serializinghtml"
1995
+ version = "2.0.0"
1996
+ source = { registry = "https://pypi.org/simple" }
1997
+ sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" }
1998
+ wheels = [
1999
+ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
2000
+ ]
2001
+
1832
2002
  [[package]]
1833
2003
  name = "stack-data"
1834
2004
  version = "0.6.3"
@@ -1875,7 +2045,7 @@ wheels = [
1875
2045
 
1876
2046
  [[package]]
1877
2047
  name = "sweatstack"
1878
- version = "0.46.0"
2048
+ version = "0.54.0"
1879
2049
  source = { editable = "." }
1880
2050
  dependencies = [
1881
2051
  { name = "email-validator" },
@@ -1899,6 +2069,10 @@ streamlit = [
1899
2069
  [package.dev-dependencies]
1900
2070
  dev = [
1901
2071
  { name = "datamodel-code-generator" },
2072
+ { name = "myst-parser" },
2073
+ { name = "sphinx" },
2074
+ { name = "sphinx-markdown-builder" },
2075
+ { name = "streamlit" },
1902
2076
  ]
1903
2077
 
1904
2078
  [package.metadata]
@@ -1917,7 +2091,22 @@ requires-dist = [
1917
2091
  provides-extras = ["streamlit", "jupyter"]
1918
2092
 
1919
2093
  [package.metadata.requires-dev]
1920
- dev = [{ name = "datamodel-code-generator", specifier = ">=0.26.5" }]
2094
+ dev = [
2095
+ { name = "datamodel-code-generator", specifier = ">=0.26.5" },
2096
+ { name = "myst-parser", specifier = ">=4.0.1" },
2097
+ { name = "sphinx", specifier = ">=8.2.3" },
2098
+ { name = "sphinx-markdown-builder", specifier = ">=0.6.8" },
2099
+ { name = "streamlit", specifier = ">=1.42.0" },
2100
+ ]
2101
+
2102
+ [[package]]
2103
+ name = "tabulate"
2104
+ version = "0.9.0"
2105
+ source = { registry = "https://pypi.org/simple" }
2106
+ sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
2107
+ wheels = [
2108
+ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
2109
+ ]
1921
2110
 
1922
2111
  [[package]]
1923
2112
  name = "tenacity"
@@ -1,6 +0,0 @@
1
- build:
2
- rm -rf dist
3
- uvx --from build pyproject-build --installer uv
4
-
5
- publish: build
6
- uvx twine upload dist/*
File without changes
File without changes
File without changes