sweatstack 0.1.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.
@@ -0,0 +1,9 @@
1
+ .env
2
+ .env.*
3
+ !.env.template
4
+ .venv
5
+ __pycache__
6
+ GCP_CREDENTIALS
7
+ **/.DS_Store
8
+ *.egg-info
9
+ .vscode
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.3
2
+ Name: sweatstack
3
+ Version: 0.1.0
4
+ Summary: The official Python library for SweatStack
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx
7
+ Requires-Dist: pandas
8
+ Requires-Dist: pydantic
9
+ Provides-Extra: dev
10
+ Requires-Dist: datamodel-code-generator; extra == 'dev'
11
+ Provides-Extra: ipython
12
+ Requires-Dist: ipython; extra == 'ipython'
13
+ Provides-Extra: parquet
14
+ Requires-Dist: pyarrow; extra == 'parquet'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # SweatStack Python Library
18
+
19
+ ## Overview
20
+
21
+ SweatStack is a powerful Python library designed for athletes, coaches, and sports scientists to analyze and manage athletic performance data. It provides a seamless interface to interact with the SweatStack API, allowing users to retrieve, analyze, and visualize activity data, user information, and performance metrics.
22
+
23
+ ## Installation
24
+
25
+ We recommend using `uv` to manage Python and install the library.
26
+ Read more about `uv` [here](https://docs.astral.sh/uv/getting-started/).
27
+
28
+ ```bash
29
+ uv pip install sweatstack
30
+ ```
31
+
32
+ You can also install it with `pip` (or `pipx`) directly.
33
+ ```bash
34
+ pip install sweatstack
35
+ ```
36
+
37
+ ## Quickstart
38
+
39
+ Get started with analyzing your latest activity:
40
+
41
+ ```python
42
+ import sweatstack as ss
43
+
44
+ ss.login()
45
+
46
+ latest_activity = ss.get_latest_activity()
47
+
48
+ print(latest_activity) # `latest_activity` is a pandas DataFrame
49
+ ```
50
+
51
+
52
+ ## Authentication
53
+
54
+ To be able to access your data in Sweat Stack, you need to authenticate the library with your Sweat Stack account.
55
+ The easiest way to do this is to use your browser to login:
56
+
57
+ ```python
58
+ import sweatstack as ss
59
+
60
+ ss.login()
61
+ ```
62
+ This will automaticallyset the appropriate authentication tokens in your Python code.
63
+
64
+ Alternatively, you can set the `SWEAT_STACK_API_KEY` environment variable to your API key.
65
+ You can create an API key [here](https://app.sweatstack.com/account/api-keys).
66
+
67
+ ```python
68
+ import os
69
+
70
+ import sweatstack as ss
71
+
72
+ os.environ["SWEAT_STACK_API_KEY"] = "your_api_key_here"
73
+
74
+ # Now you can use the library
75
+ ```
76
+
77
+
78
+ ## Listing activities
79
+
80
+ To list activities, you can use the `list_activities()` function:
81
+
82
+ ```python
83
+ for activity in ss.list_activities():
84
+ print(activity)
85
+ ```
86
+ > **Info:** This method returns a summary of the activities, not the actual timeseries data.
87
+ > To get the actual data, you need to use the `get_activity_data()` or `get_latest_activity_data()`) methods documented below.
88
+
89
+ ## Getting activity summaries
90
+
91
+ To get the summary of an activity, you can use the `get_activity()` function:
92
+
93
+ ```python
94
+ activity = ss.get_activity(activity_id)
95
+ print(activity)
96
+ ```
97
+
98
+ To quickly the latest activity, you can use the `get_latest_activity()` function:
99
+
100
+ ```python
101
+ activity = ss.get_latest_activity()
102
+ print(activity)
103
+ ```
104
+
105
+ ## Getting activity data
106
+
107
+ To get the timeseries data of one activity, you can use the `get_activity_data()` method:
108
+
109
+ ```python
110
+ data = ss.get_activity_data(activity_id)
111
+ print(data)
112
+ ```
113
+
114
+ This method returns a pandas DataFrame.
115
+ If your are not familiar with pandas and/or DataFrames, start by reading this [introduction](https://pandas.pydata.org/docs/user_guide/10min.html).
116
+
117
+ Similar as for the summaries, you can use the `get_latest_activity_data()` method to get the timeseries data of the latest activity:
118
+
119
+ ```python
120
+ data = ss.get_latest_activity_data()
121
+ print(data)
122
+ ```
123
+
124
+ To get the timeseries data of multiple activities, you can use the `get_longitudinal_data()` method:
125
+
126
+ ```python
127
+ longitudinal_data = ss.get_longitudinal_data(
128
+ start=date.today() - timedelta(days=180),
129
+ sport="running",
130
+ metrics=["power", "heart_rate"],
131
+ )
132
+ print(longitudinal_data)
133
+ ```
134
+
135
+ Because the result of `get_longitudinal_data()` can be very large, the data is retrieved in a compressed format (parquet) that requires the `pyarrow` library to be installed. If you intend to use this method, make sure to install the `sweatstack` libraryr with `uv pip install sweatstack[parquet]`.
136
+ Also note that depending on the amount of data that you requested, this might take a while.
137
+
138
+ ## Accessing other user's data
139
+
140
+ By default, the library will give you access to your own data.
141
+
142
+ You can list all users you have access to with the `list_accessible_users()` method:
143
+
144
+ ```python
145
+ for user in ss.list_accessible_users():
146
+ print(user)
147
+ ```
148
+
149
+ You can switch to another user by using the `switch_user()` method:
150
+
151
+ ```python
152
+ ss.switch_user(user)
153
+ ```
154
+
155
+ Calling any of the methods above will return the data for the user you switched to.
156
+
157
+ You can easily switch back to your original user by calling the `switch_to_root_user()` method:
158
+
159
+ ```python
160
+ ss.switch_to_root_user()
161
+ ```
162
+
163
+
164
+ ## Metrics
165
+
166
+ The API supports the following metrics:
167
+ - `power`: Power in Watt
168
+ - `speed`: Speed in m/s
169
+ - `heart_rate`: Heart rate in BPM
170
+ - `smo2`: Muscle oxygen saturation in %
171
+ - `core_temperature`: Core body temperature in °C
172
+ - `altitude`: Altitude in meters
173
+ - `cadence`: Cadence in RPM
174
+ - `temperature`: Ambient temperature in °C
175
+ - `distance`: Distance in m
176
+ - `longitude`: Longitude in degrees
177
+ - `latitude`: Latitude in degrees
178
+
179
+
180
+ ## Sports
181
+
182
+ The API supports the following sports:
183
+ - `running`: Running
184
+ - `cycling`: Cycling
185
+
186
+ More sports will be added in the future.
@@ -0,0 +1,170 @@
1
+ # SweatStack Python Library
2
+
3
+ ## Overview
4
+
5
+ SweatStack is a powerful Python library designed for athletes, coaches, and sports scientists to analyze and manage athletic performance data. It provides a seamless interface to interact with the SweatStack API, allowing users to retrieve, analyze, and visualize activity data, user information, and performance metrics.
6
+
7
+ ## Installation
8
+
9
+ We recommend using `uv` to manage Python and install the library.
10
+ Read more about `uv` [here](https://docs.astral.sh/uv/getting-started/).
11
+
12
+ ```bash
13
+ uv pip install sweatstack
14
+ ```
15
+
16
+ You can also install it with `pip` (or `pipx`) directly.
17
+ ```bash
18
+ pip install sweatstack
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ Get started with analyzing your latest activity:
24
+
25
+ ```python
26
+ import sweatstack as ss
27
+
28
+ ss.login()
29
+
30
+ latest_activity = ss.get_latest_activity()
31
+
32
+ print(latest_activity) # `latest_activity` is a pandas DataFrame
33
+ ```
34
+
35
+
36
+ ## Authentication
37
+
38
+ To be able to access your data in Sweat Stack, you need to authenticate the library with your Sweat Stack account.
39
+ The easiest way to do this is to use your browser to login:
40
+
41
+ ```python
42
+ import sweatstack as ss
43
+
44
+ ss.login()
45
+ ```
46
+ This will automaticallyset the appropriate authentication tokens in your Python code.
47
+
48
+ Alternatively, you can set the `SWEAT_STACK_API_KEY` environment variable to your API key.
49
+ You can create an API key [here](https://app.sweatstack.com/account/api-keys).
50
+
51
+ ```python
52
+ import os
53
+
54
+ import sweatstack as ss
55
+
56
+ os.environ["SWEAT_STACK_API_KEY"] = "your_api_key_here"
57
+
58
+ # Now you can use the library
59
+ ```
60
+
61
+
62
+ ## Listing activities
63
+
64
+ To list activities, you can use the `list_activities()` function:
65
+
66
+ ```python
67
+ for activity in ss.list_activities():
68
+ print(activity)
69
+ ```
70
+ > **Info:** This method returns a summary of the activities, not the actual timeseries data.
71
+ > To get the actual data, you need to use the `get_activity_data()` or `get_latest_activity_data()`) methods documented below.
72
+
73
+ ## Getting activity summaries
74
+
75
+ To get the summary of an activity, you can use the `get_activity()` function:
76
+
77
+ ```python
78
+ activity = ss.get_activity(activity_id)
79
+ print(activity)
80
+ ```
81
+
82
+ To quickly the latest activity, you can use the `get_latest_activity()` function:
83
+
84
+ ```python
85
+ activity = ss.get_latest_activity()
86
+ print(activity)
87
+ ```
88
+
89
+ ## Getting activity data
90
+
91
+ To get the timeseries data of one activity, you can use the `get_activity_data()` method:
92
+
93
+ ```python
94
+ data = ss.get_activity_data(activity_id)
95
+ print(data)
96
+ ```
97
+
98
+ This method returns a pandas DataFrame.
99
+ If your are not familiar with pandas and/or DataFrames, start by reading this [introduction](https://pandas.pydata.org/docs/user_guide/10min.html).
100
+
101
+ Similar as for the summaries, you can use the `get_latest_activity_data()` method to get the timeseries data of the latest activity:
102
+
103
+ ```python
104
+ data = ss.get_latest_activity_data()
105
+ print(data)
106
+ ```
107
+
108
+ To get the timeseries data of multiple activities, you can use the `get_longitudinal_data()` method:
109
+
110
+ ```python
111
+ longitudinal_data = ss.get_longitudinal_data(
112
+ start=date.today() - timedelta(days=180),
113
+ sport="running",
114
+ metrics=["power", "heart_rate"],
115
+ )
116
+ print(longitudinal_data)
117
+ ```
118
+
119
+ Because the result of `get_longitudinal_data()` can be very large, the data is retrieved in a compressed format (parquet) that requires the `pyarrow` library to be installed. If you intend to use this method, make sure to install the `sweatstack` libraryr with `uv pip install sweatstack[parquet]`.
120
+ Also note that depending on the amount of data that you requested, this might take a while.
121
+
122
+ ## Accessing other user's data
123
+
124
+ By default, the library will give you access to your own data.
125
+
126
+ You can list all users you have access to with the `list_accessible_users()` method:
127
+
128
+ ```python
129
+ for user in ss.list_accessible_users():
130
+ print(user)
131
+ ```
132
+
133
+ You can switch to another user by using the `switch_user()` method:
134
+
135
+ ```python
136
+ ss.switch_user(user)
137
+ ```
138
+
139
+ Calling any of the methods above will return the data for the user you switched to.
140
+
141
+ You can easily switch back to your original user by calling the `switch_to_root_user()` method:
142
+
143
+ ```python
144
+ ss.switch_to_root_user()
145
+ ```
146
+
147
+
148
+ ## Metrics
149
+
150
+ The API supports the following metrics:
151
+ - `power`: Power in Watt
152
+ - `speed`: Speed in m/s
153
+ - `heart_rate`: Heart rate in BPM
154
+ - `smo2`: Muscle oxygen saturation in %
155
+ - `core_temperature`: Core body temperature in °C
156
+ - `altitude`: Altitude in meters
157
+ - `cadence`: Cadence in RPM
158
+ - `temperature`: Ambient temperature in °C
159
+ - `distance`: Distance in m
160
+ - `longitude`: Longitude in degrees
161
+ - `latitude`: Latitude in degrees
162
+
163
+
164
+ ## Sports
165
+
166
+ The API supports the following sports:
167
+ - `running`: Running
168
+ - `cycling`: Cycling
169
+
170
+ More sports will be added in the future.
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "sweatstack"
3
+ version = "0.1.0"
4
+ description = "The official Python library for SweatStack"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "httpx",
9
+ "pandas",
10
+ "pydantic",
11
+ ]
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "datamodel-code-generator",
21
+ ]
22
+ parquet = [
23
+ "pyarrow",
24
+ ]
25
+ ipython = [
26
+ "ipython",
27
+ ]
28
+
29
+ [project.scripts]
30
+ generate-response-models = "sweatstack.cli:generate_response_models"
31
+ sweatshell = "sweatstack.cli:run_ipython"
@@ -0,0 +1,25 @@
1
+ import os
2
+ from datetime import date
3
+
4
+ from SweatStack import SweatStack
5
+
6
+
7
+ os.environ["SWEAT_STACK_API_KEY"] = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1R1A4SHhPc3R0NVVCZVgyMWVaOCIsImF6cCI6InVHUDhIeE9zdHQ1VUJlWDIxZVo4IiwiZXhwIjoxNzIwOTAzODM1LCJpYXQiOjE3MjAwMDM4MzV9.Nk4pWhTz3-qJpgvVlseI5FklBwjROS7GTaPN7gy7budia_0qWZZYx_8_cidhdrWfXZY7tOWPWv82yF9RZ3SQbmWxZbk9sydTKOKxX2g4mbj4WhbWg-muhU_BiIMMTZ-HtrWcesr_daoUZJuRUht8lzxHWsUT4cpleOGdN_yI9Wqcn_ZIr1njhRIXa8MaBWO0bxolpNa9a8iKxhUWw5sVJQaVIYidN0puhqaXCqZrrZNntdASbhCmHfWJeIeWlASZ7gtbGIaHTuI08tCrMZEj4Y0i-mAulT_zDtNNTevx6yL9LFSCuWl75euCLEph_2Ncw1yKzGB-wTSb6bP-solNSw"
8
+ # os.environ["SWEAT_STACK_URL"] = "http://localhost:2400"
9
+
10
+ def main():
11
+ client = SweatStack()
12
+ import time
13
+ t0 = time.time()
14
+ awd = client.get_accumulated_work_duration(
15
+ start=date(2024, 1, 1),
16
+ end=date(2024, 8, 1),
17
+ sport="running",
18
+ metric="power",
19
+ )
20
+ print(f"This took: {round(time.time() - t0, 2)} seconds")
21
+ print(awd)
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,26 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="sweatstack",
5
+ version='0.1',
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "httpx",
9
+ "pandas",
10
+ "pydantic",
11
+ ],
12
+ extras_require={
13
+ "parquet": [
14
+ "pyarrow",
15
+ ],
16
+ # 'plotting': ['matplotlib>=3.0'], # Optional plotting feature
17
+ "dev": [
18
+ "datamodel-code-generator",
19
+ ],
20
+ },
21
+ entry_points={
22
+ "console_scripts": [
23
+ "generate-response-models = sweatstack.cli:generate_response_models"
24
+ ]
25
+ },
26
+ )
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from python-library!"
@@ -0,0 +1 @@
1
+ from .client import *
@@ -0,0 +1,38 @@
1
+ import os
2
+ import httpx
3
+ from pathlib import Path
4
+
5
+ from IPython import start_ipython
6
+
7
+ from datamodel_code_generator import InputFileType, generate
8
+ from datamodel_code_generator import DataModelType
9
+
10
+
11
+ def generate_response_models():
12
+ response = httpx.get("http://localhost:2400/openapi.json")
13
+ response.raise_for_status()
14
+ output_directory = Path(__file__).parent
15
+ output = Path(output_directory / "schemas.py")
16
+ output.unlink(missing_ok=True)
17
+ generate(
18
+ response.text,
19
+ input_file_type=InputFileType.OpenAPI,
20
+ input_filename="openapi.json",
21
+ output=output,
22
+ # set up the output model types
23
+ output_model_type=DataModelType.PydanticV2BaseModel,
24
+ )
25
+
26
+ model = output.read_text()
27
+ print(model)
28
+
29
+
30
+ def run_ipython():
31
+ script_dir = os.path.dirname(os.path.abspath(__file__))
32
+ startup_script = os.path.join(script_dir, 'ipython_init.py')
33
+
34
+ ipython_args = [
35
+ '--InteractiveShellApp.exec_files={}'.format(startup_script)
36
+ ]
37
+
38
+ start_ipython(argv=ipython_args)
@@ -0,0 +1,383 @@
1
+
2
+ import random
3
+ import os
4
+ import webbrowser
5
+ from http.server import HTTPServer, BaseHTTPRequestHandler
6
+ from io import BytesIO, StringIO
7
+ from contextlib import contextmanager
8
+ from datetime import date, datetime, timedelta, timezone
9
+ from typing import Dict, Iterator, List,Union
10
+ from urllib.parse import urlparse, parse_qs
11
+
12
+ import httpx
13
+ import pandas as pd
14
+ try:
15
+ import pyarrow
16
+ except ImportError:
17
+ pyarrow = None
18
+
19
+
20
+ from .schemas import ActivityDetail, ActivitySummary, Metric, PermissionType, Sport, User
21
+
22
+
23
+ AUTH_SUCCESSFUL_RESPONSE = """
24
+ <!DOCTYPE html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="UTF-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
+ <title>Authorization Successful</title>
30
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tachyons/4.11.1/tachyons.min.css">
31
+ </head>
32
+ <body class="bg-light-gray vh-100 flex items-center justify-center">
33
+ <article class="mw6 center bg-white br3 pa3 pa4-ns mv3 ba b--black-10">
34
+ <div class="tc">
35
+ <div class="flex justify-center items-center">
36
+ <img src="https://sweatstack.no/images/favicon-white-bg-small.png" alt="Sweat Stack Logo" class="h4 w4 dib pa2 ml2">
37
+ <div class="f1 b black ph3">❤️</div>
38
+ <img src="https://s3.dualstack.us-east-2.amazonaws.com/pythondotorg-assets/media/community/logos/python-logo-only.png" alt="Python Logo" class="h4 w4 dib pa2 ml2">
39
+ </div>
40
+ <h1 class="f2 mb2">Sweat Stack Python login successful</h1>
41
+ </div>
42
+ <p class="lh-copy measure center f4 black-70">
43
+ You can now close this window and return to your Python code.
44
+ Window auto-closing in 5s...<br>
45
+ </p>
46
+ </article>
47
+ <script>
48
+ setTimeout(() => window.close(), 5000);
49
+ </script>
50
+ </body>
51
+ </html>
52
+ """
53
+
54
+
55
+ JWT_ENV_VARIABLE = "SWEAT_STACK_API_KEY"
56
+
57
+ try:
58
+ SWEAT_STACK_URL = os.environ["SWEAT_STACK_URL"]
59
+ except KeyError:
60
+ SWEAT_STACK_URL = "https://sweat-stack-s7c65i4gka-ew.a.run.app"
61
+
62
+
63
+ SWEAT_STACK_URL = "http://localhost:2400"
64
+
65
+
66
+ class SweatStack:
67
+ def __init__(self):
68
+ self.jwt = os.environ.get(JWT_ENV_VARIABLE)
69
+ self.root_jwt = self.jwt
70
+
71
+ def login(self):
72
+ class AuthHandler(BaseHTTPRequestHandler):
73
+ def log_message(self, format, *args):
74
+ # Override to disable logging
75
+ pass
76
+
77
+ def do_GET(self):
78
+ query = urlparse(self.path).query
79
+ params = parse_qs(query)
80
+
81
+ if "jwt" in params:
82
+ self.server.jwt = params["jwt"][0]
83
+ self.send_response(200)
84
+ self.send_header("Content-type", "text/html")
85
+ self.end_headers()
86
+ self.wfile.write(AUTH_SUCCESSFUL_RESPONSE.encode())
87
+ self.server.server_close()
88
+
89
+ # Find an available port
90
+ while True:
91
+ port = random.randint(8000, 9000)
92
+ try:
93
+ server = HTTPServer(("localhost", port), AuthHandler)
94
+ break
95
+ except OSError:
96
+ continue
97
+
98
+ authorization_url = f"{SWEAT_STACK_URL}/auth/authorize-script?redirect_port={port}"
99
+ webbrowser.open(authorization_url)
100
+
101
+ print(f"Waiting for authorization... (listening on port {port})")
102
+ print(f"If not redirected, open the following URL in your browser: {authorization_url}")
103
+ print("")
104
+
105
+ server.timeout = 30
106
+ try:
107
+ server.handle_request()
108
+ except TimeoutError:
109
+ raise Exception("Sweat Stack Python login timed out after 30 seconds. Please try again.")
110
+
111
+ if hasattr(server, "jwt"):
112
+ self.jwt = server.jwt
113
+ print(f"Sweat Stack Python login successful.")
114
+ else:
115
+ raise Exception("Sweat Stack Python login failed. Please try again.")
116
+
117
+ @contextmanager
118
+ def _httpx_client(self):
119
+ headers = {
120
+ "authorization": f"Bearer {self.jwt}"
121
+ }
122
+ with httpx.Client(
123
+ base_url=SWEAT_STACK_URL,
124
+ headers=headers,
125
+ timeout=30,
126
+ ) as client:
127
+ yield client
128
+
129
+ def list_users(self, permission_type: Union[PermissionType, str] = None) -> List[User]:
130
+ if permission_type is not None:
131
+ params = {"type": permission_type.value if isinstance(permission_type, PermissionType) else permission_type}
132
+ else:
133
+ params = {}
134
+
135
+ with self._httpx_client() as client:
136
+ response = client.get("/api/users/", params=params)
137
+ users = response.json()
138
+
139
+ return [User.model_validate(user) for user in users]
140
+
141
+ def list_accessible_users(self) -> List[User]:
142
+ return self.list_users(permission_type=PermissionType.received)
143
+
144
+ def whoami(self) -> User:
145
+ with self._httpx_client() as client:
146
+ response = client.get("/api/users/me")
147
+ return User.model_validate(response.json())
148
+
149
+ def get_delegated_token(self, user: Union[User, str]):
150
+ if isinstance(user, str):
151
+ user_id = user
152
+ else:
153
+ user_id = user.id
154
+
155
+ with self._httpx_client() as client:
156
+ response = client.get(
157
+ f"/api/users/{user_id}/delegated-token",
158
+ )
159
+ response.raise_for_status()
160
+ return response.json()["jwt"]
161
+
162
+ def switch_user(self, user: Union[User, str]):
163
+ self.root_jwt = self.jwt
164
+ self.jwt = self.get_delegated_token(user)
165
+
166
+ def switch_to_root_user(self):
167
+ """
168
+ Switch back to the root user by setting the JWT to the root JWT.
169
+ """
170
+ self.jwt = self.root_jwt
171
+
172
+ def _check_timezone_aware(self, date_obj: Union[date, datetime]):
173
+ if not isinstance(date_obj, date) and date_obj.tzinfo is None and date_obj.tzinfo.utcoffset(date_obj) is None:
174
+ return date_obj.replace(tzinfo=timezone.utc)
175
+ else:
176
+ return date_obj
177
+
178
+ def _fetch_activities(
179
+ self,
180
+ sport: Union[Sport, str] = None,
181
+ start: Union[date, datetime] = None,
182
+ end: Union[date, datetime] = None,
183
+ limit: int = None,
184
+ as_pydantic: bool = False,
185
+ ) -> Iterator[Union[Dict, ActivitySummary]]:
186
+ activities_count = 0
187
+
188
+ params = {}
189
+ if sport is not None:
190
+ if isinstance(sport, Sport):
191
+ sport = sport.value
192
+ params["sport"] = sport
193
+
194
+ if start is not None:
195
+ params["start"] = self._check_timezone_aware(start).isoformat()
196
+
197
+ if end is not None:
198
+ params["end"] = self._check_timezone_aware(end).isoformat()
199
+
200
+ with self._httpx_client() as client:
201
+ step_size = 50
202
+ offset = 0
203
+
204
+ while True:
205
+ params["limit"] = step_size
206
+ params["offset"] = offset
207
+ response = client.get("/api/activities/", params=params)
208
+ activities = response.json()
209
+
210
+ for activity in activities:
211
+ activities_count += 1
212
+ if limit is not None and activities_count > limit:
213
+ break
214
+ yield ActivitySummary(**activity) if as_pydantic else activity
215
+
216
+ if limit is not None and activities_count > limit or len(activities) < step_size:
217
+ break
218
+
219
+ offset += step_size
220
+
221
+
222
+ def list_activities(self, sport: Union[Sport, str] = None, start: Union[date, datetime] = None, end: Union[date, datetime] = None, limit: int = None, as_dataframe: bool = True) -> Union[Iterator[Dict], pd.DataFrame]:
223
+ if as_dataframe:
224
+ return pd.DataFrame(self._fetch_activities(limit=limit))
225
+ else:
226
+ return self._fetch_activities(
227
+ sport=sport,
228
+ start=start,
229
+ end=end,
230
+ limit=limit,
231
+ as_pydantic=True,
232
+ )
233
+
234
+ def get_longitudinal_data(
235
+ self,
236
+ sport: Union[Sport, str],
237
+ metrics: List[Union[Metric, str]],
238
+ start: Union[date, datetime] = None,
239
+ end: Union[date, datetime] = None,
240
+ ) -> pd.DataFrame:
241
+
242
+ params = {}
243
+ if sport is not None:
244
+ if isinstance(sport, Sport):
245
+ sport = sport.value
246
+ params["sport"] = sport
247
+
248
+ if metrics is not None:
249
+ new_metrics = []
250
+ for metric in metrics:
251
+ if isinstance(metric, Metric):
252
+ new_metrics.append(metric.value)
253
+ else:
254
+ new_metrics.append(metric)
255
+ params["metrics"] = new_metrics
256
+
257
+ if start is not None:
258
+ params["start"] = self._check_timezone_aware(start).isoformat()
259
+ else:
260
+ params["start"] = (date.today() - timedelta(days=30)).isoformat()
261
+
262
+ if end is not None:
263
+ params["end"] = self._check_timezone_aware(end).isoformat()
264
+
265
+
266
+ with self._httpx_client() as client:
267
+ response = client.get(f"/api/activities/timeseries", params=params)
268
+ buffer = BytesIO(response.content)
269
+ data = pd.read_parquet(buffer, engine="pyarrow")
270
+ return data
271
+
272
+ def get_activity(self, activity_id: str) -> ActivityDetail:
273
+ with self._httpx_client() as client:
274
+ response = client.get(f"/api/activities/{activity_id}")
275
+ return ActivityDetail(**response.json())
276
+
277
+ def get_latest_activity(self) -> ActivityDetail:
278
+ activity = next(self._fetch_activities(limit=1, as_pydantic=True))
279
+ return self.get_activity(activity.id)
280
+
281
+ def get_activity_data(self, activity_id: str) -> pd.DataFrame:
282
+ with self._httpx_client() as client:
283
+ response = client.get(f"/api/activities/{activity_id}/timeseries")
284
+ data = pd.read_json(StringIO(response.json()), orient="split")
285
+ data.index = pd.to_datetime(data.index)
286
+ return data
287
+
288
+ def get_latest_activity_data(self) -> pd.DataFrame:
289
+ activity = self.get_latest_activity()
290
+ return self.get_activity_data(activity.id)
291
+
292
+ def get_accumulated_work_duration(self, start: date, sport: Union[Sport, str], metric: Union[Metric, str], end: date=None) -> pd.DataFrame:
293
+ if not isinstance(start, date):
294
+ start = date.fromisoformat(start)
295
+
296
+ if end is None:
297
+ end = date.today()
298
+ if not isinstance(end, date):
299
+ end = date.fromisoformat(end)
300
+
301
+ if not isinstance(metric, Metric):
302
+ metric = Metric(metric)
303
+ if not isinstance(sport, Sport):
304
+ sport = Sport(sport)
305
+
306
+ with self._httpx_client() as client:
307
+ response = client.get(
308
+ "/api/activities/accumulated-work-duration",
309
+ params={
310
+ "start": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
311
+ "end": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
312
+ "sport": sport.value,
313
+ "metric": metric.value,
314
+ }
315
+ )
316
+
317
+ awd = pd.read_json(
318
+ StringIO(response.json()),
319
+ orient="split",
320
+ date_unit="s",
321
+ typ="series",
322
+ )
323
+ awd = pd.to_timedelta(awd, unit="seconds")
324
+ awd.name = "duration"
325
+ awd.index.name = metric.value
326
+ return awd
327
+
328
+ def get_mean_max(self, start: date, sport: Union[Sport, str], metric: Union[Metric, str], end: date=None) -> pd.DataFrame:
329
+ if not isinstance(start, date):
330
+ start = date.fromisoformat(start)
331
+
332
+ if end is None:
333
+ end = date.today()
334
+ if not isinstance(end, date):
335
+ end = date.fromisoformat(end)
336
+
337
+ if not isinstance(metric, Metric):
338
+ metric = Metric(metric)
339
+ if not isinstance(sport, Sport):
340
+ sport = Sport(sport)
341
+
342
+ with self._httpx_client() as client:
343
+ response = client.get(
344
+ "/api/activities/mean-max",
345
+ params={
346
+ "start": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
347
+ "end": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
348
+ "sport": sport.value,
349
+ "metric": metric.value,
350
+ }
351
+ )
352
+
353
+ mean_max = pd.read_json(
354
+ StringIO(response.json()),
355
+ orient="split",
356
+ date_unit="s",
357
+ typ="series",
358
+ )
359
+ mean_max = pd.to_timedelta(mean_max, unit="seconds")
360
+ mean_max.name = "duration"
361
+ mean_max.index.name = metric.value
362
+ return mean_max
363
+
364
+
365
+ _instance = SweatStack()
366
+
367
+
368
+ login = _instance.login
369
+ list_users = _instance.list_users
370
+ list_accessible_users = _instance.list_accessible_users
371
+ switch_user = _instance.switch_user
372
+ switch_to_root_user = _instance.switch_to_root_user
373
+ whoami = _instance.whoami
374
+
375
+ list_activities = _instance.list_activities
376
+ get_activity = _instance.get_activity
377
+ get_latest_activity = _instance.get_latest_activity
378
+ get_activity_data = _instance.get_activity_data
379
+ get_latest_activity_data = _instance.get_latest_activity_data
380
+
381
+ get_accumulated_work_duration = _instance.get_accumulated_work_duration
382
+ get_mean_max = _instance.get_mean_max
383
+ get_longitudinal_data = _instance.get_longitudinal_data
@@ -0,0 +1,12 @@
1
+ import time
2
+
3
+ import sweatstack as ss
4
+
5
+
6
+ print("\n")
7
+ print(">>>>>>>>>> Sweat Stack Initialization <<<<<<<<<")
8
+ print("Initializing....")
9
+ print("You will be redirected to your browser for authentication.\n")
10
+ time.sleep(2)
11
+
12
+ ss.login()
@@ -0,0 +1,167 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: openapi.json
3
+ # timestamp: 2024-08-29T12:58:42+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import List, Optional, Union
9
+
10
+ from pydantic import AwareDatetime, BaseModel, Field, conint
11
+
12
+
13
+ class ActivityLap(BaseModel):
14
+ start: AwareDatetime = Field(..., title='Start')
15
+ end: AwareDatetime = Field(..., title='End')
16
+ power: Optional[float] = Field(None, title='Power')
17
+ speed: Optional[float] = Field(None, title='Speed')
18
+ distance: Optional[float] = Field(None, title='Distance')
19
+ altitude: Optional[float] = Field(None, title='Altitude')
20
+ heart_rate: Optional[float] = Field(None, title='Heart Rate')
21
+ heart_rate_start: Optional[float] = Field(None, title='Heart Rate Start')
22
+ heart_rate_end: Optional[float] = Field(None, title='Heart Rate End')
23
+ cadence: Optional[float] = Field(None, title='Cadence')
24
+ temperature: Optional[float] = Field(None, title='Temperature')
25
+ core_temperature: Optional[float] = Field(None, title='Core Temperature')
26
+ smo2: Optional[float] = Field(None, title='Smo2')
27
+ duration: str = Field(..., title='Duration')
28
+
29
+
30
+ class DelegatedTokenResponse(BaseModel):
31
+ jwt: str = Field(..., title='Jwt')
32
+
33
+
34
+ class JWTResponse(BaseModel):
35
+ jwt: str = Field(..., title='Jwt')
36
+ refresh_token: str = Field(..., title='Refresh Token')
37
+
38
+
39
+ class LapSyncData(BaseModel):
40
+ power: Optional[float] = Field(None, title='Power')
41
+ speed: Optional[float] = Field(None, title='Speed')
42
+ sport: str = Field(..., title='Sport')
43
+ activity: str = Field(..., title='Activity')
44
+
45
+
46
+ class LapSyncedTrace(BaseModel):
47
+ timestamp: AwareDatetime = Field(..., title='Timestamp')
48
+ lactate: Optional[float] = Field(None, title='Lactate')
49
+ rpe: Optional[conint(ge=0, le=10)] = Field(
50
+ None,
51
+ description='Rating of Perceived Exertion (RPE) on the CR10 scale, ranging from 0 to 10:\n0 - No exertion at all\n1 - Very light\n2 - Light\n3 - Moderate\n4 - Somewhat hard\n5 - Hard\n6 - \n7 - Very hard\n8 - \n9 - Very, very hard\n10 - Maximum effort',
52
+ title='Rpe',
53
+ )
54
+ notes: Optional[str] = Field(None, title='Notes')
55
+ lap_sync: Optional[LapSyncData] = None
56
+
57
+
58
+ class Metric(Enum):
59
+ power = 'power'
60
+ speed = 'speed'
61
+ heart_rate = 'heart_rate'
62
+ smo2 = 'smo2'
63
+ core_temperature = 'core_temperature'
64
+ altitude = 'altitude'
65
+ cadence = 'cadence'
66
+ temperature = 'temperature'
67
+ distance = 'distance'
68
+ longitude = 'longitude'
69
+ latitude = 'latitude'
70
+ lactate = 'lactate'
71
+ rpe = 'rpe'
72
+ notes = 'notes'
73
+
74
+
75
+ class PermissionType(Enum):
76
+ granted = 'granted'
77
+ received = 'received'
78
+
79
+
80
+ class Sport(Enum):
81
+ cycling = 'cycling'
82
+ cycling_road = 'cycling.road'
83
+ cycling_tt = 'cycling.tt'
84
+ cycling_cyclocross = 'cycling.cyclocross'
85
+ cycling_gravel = 'cycling.gravel'
86
+ cycling_mountainbike = 'cycling.mountainbike'
87
+ cycling_track = 'cycling.track'
88
+ cycling_track_250m = 'cycling.track.250m'
89
+ cycling_track_333m = 'cycling.track.333m'
90
+ running = 'running'
91
+ running_road = 'running.road'
92
+ running_track = 'running.track'
93
+ running_track_200m = 'running.track.200m'
94
+ running_track_400m = 'running.track.400m'
95
+ running_trail = 'running.trail'
96
+ walking = 'walking'
97
+ hiking = 'hiking'
98
+ cross_country_skiing = 'cross_country_skiing'
99
+ cross_country_skiing_classic = 'cross_country_skiing.classic'
100
+ cross_country_skiing_skate = 'cross_country_skiing.skate'
101
+ cross_country_skiing_backcountry = 'cross_country_skiing.backcountry'
102
+ rowing = 'rowing'
103
+ swimming = 'swimming'
104
+ swimming_pool = 'swimming.pool'
105
+ swimming_pool_50m = 'swimming.pool.50m'
106
+ swimming_pool_25m = 'swimming.pool.25m'
107
+ swimming_pool_25y = 'swimming.pool.25y'
108
+ swimming_pool_33m = 'swimming.pool.33m'
109
+ swimming_open_water = 'swimming.open_water'
110
+ swimming_flume = 'swimming.flume'
111
+ generic = 'generic'
112
+
113
+
114
+ class Trace(BaseModel):
115
+ timestamp: AwareDatetime = Field(..., title='Timestamp')
116
+ lactate: Optional[float] = Field(None, title='Lactate')
117
+ rpe: Optional[conint(ge=0, le=10)] = Field(
118
+ None,
119
+ description='Rating of Perceived Exertion (RPE) on the CR10 scale, ranging from 0 to 10:\n0 - No exertion at all\n1 - Very light\n2 - Light\n3 - Moderate\n4 - Somewhat hard\n5 - Hard\n6 - \n7 - Very hard\n8 - \n9 - Very, very hard\n10 - Maximum effort',
120
+ title='Rpe',
121
+ )
122
+ notes: Optional[str] = Field(None, title='Notes')
123
+
124
+
125
+ class User(BaseModel):
126
+ id: str = Field(..., title='Id')
127
+ first_name: Optional[str] = Field(None, title='First Name')
128
+ last_name: Optional[str] = Field(None, title='Last Name')
129
+ display_name: Optional[str] = Field(None, title='Display Name')
130
+ permission_types: Optional[List[PermissionType]] = Field(
131
+ None, title='Permission Types'
132
+ )
133
+
134
+
135
+ class ValidationError(BaseModel):
136
+ loc: List[Union[str, int]] = Field(..., title='Location')
137
+ msg: str = Field(..., title='Message')
138
+ type: str = Field(..., title='Error Type')
139
+
140
+
141
+ class ActivityDetail(BaseModel):
142
+ id: str = Field(..., title='Id')
143
+ title: Optional[str] = Field(None, title='Title')
144
+ start: AwareDatetime = Field(..., title='Start')
145
+ end: Optional[AwareDatetime] = Field(..., title='End')
146
+ sport: Sport
147
+ stationary: Optional[bool] = Field(None, title='Stationary')
148
+ metrics: Optional[List[Metric]] = Field([], title='Metrics')
149
+ laps: Optional[List[ActivityLap]] = Field([], title='Laps')
150
+ duration: Optional[str] = Field(..., title='Duration')
151
+ display_sport: Optional[str] = Field(..., title='Display Sport')
152
+
153
+
154
+ class ActivitySummary(BaseModel):
155
+ id: str = Field(..., title='Id')
156
+ title: Optional[str] = Field(None, title='Title')
157
+ start: AwareDatetime = Field(..., title='Start')
158
+ end: Optional[AwareDatetime] = Field(..., title='End')
159
+ sport: Sport
160
+ stationary: Optional[bool] = Field(None, title='Stationary')
161
+ metrics: Optional[List[Metric]] = Field([], title='Metrics')
162
+ duration: Optional[str] = Field(..., title='Duration')
163
+ display_sport: Optional[str] = Field(..., title='Display Sport')
164
+
165
+
166
+ class HTTPValidationError(BaseModel):
167
+ detail: Optional[List[ValidationError]] = Field(None, title='Detail')
@@ -0,0 +1,91 @@
1
+ from datetime import date, timedelta
2
+ import sweatstack as ss
3
+
4
+
5
+ import time
6
+
7
+ start = time.time()
8
+ ss.login()
9
+
10
+ activities = list(ss.list_activities(as_dataframe=False))
11
+ end = time.time()
12
+ print(f"Time taken: {end - start} seconds")
13
+ print(f"Number of activities: {len(activities)}")
14
+
15
+
16
+ start = time.time()
17
+ activities = ss.list_activities(as_dataframe=True)
18
+ end = time.time()
19
+
20
+ print(f"Time taken: {end - start} seconds")
21
+ print(f"Number of activities: {len(activities)}")
22
+ print(activities.head())
23
+
24
+
25
+ latest_activity = ss.get_latest_activity()
26
+ print(f"{latest_activity=}")
27
+
28
+
29
+ awd = ss.get_accumulated_work_duration(
30
+ start=date.today() - timedelta(days=90),
31
+ sport="running",
32
+ metric="power",
33
+ )
34
+ print(f"{awd=}")
35
+
36
+
37
+ mean_max = ss.get_mean_max(
38
+ start=date.today() - timedelta(days=90),
39
+ sport="cycling",
40
+ metric="power",
41
+ )
42
+ print(f"{mean_max=}")
43
+
44
+ users = ss.list_users()
45
+ print(f"{users=}")
46
+
47
+ users = ss.list_accessible_users()
48
+ print(f"{users=}")
49
+ print("")
50
+
51
+ for user in users:
52
+ if user.last_name.lower() == "nistad":
53
+ jon_helge = user
54
+ break
55
+ else:
56
+ raise Exception("Did not find Jon Helge!")
57
+
58
+ user = ss.whoami()
59
+ print(f"WHOAMI {user=}")
60
+ print("")
61
+ ss.switch_user(jon_helge)
62
+
63
+ user = ss.whoami()
64
+ print(f"WHOAMI {user=}")
65
+ print("")
66
+
67
+ activity = ss.get_latest_activity()
68
+ print(f"{activity=}")
69
+ print("")
70
+
71
+ user = ss.whoami()
72
+ print(f"WHOAMI {user=}")
73
+ print("")
74
+
75
+ ss.switch_to_root_user()
76
+ user = ss.whoami()
77
+ print(f"WHOAMI {user=}")
78
+ print("")
79
+
80
+ activity = ss.get_latest_activity()
81
+ print(f"{activity=}")
82
+
83
+ data = ss.get_latest_activity_data()
84
+ print(f"{data=}")
85
+
86
+ longitudinal_data = ss.get_longitudinal_data(
87
+ start=date.today() - timedelta(days=180),
88
+ sport="running",
89
+ metrics=["power", "heart_rate", "speed"],
90
+ )
91
+ print(f"{longitudinal_data.head()=}")