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.
- sweatstack-0.1.0/.gitignore +9 -0
- sweatstack-0.1.0/PKG-INFO +186 -0
- sweatstack-0.1.0/README.md +170 -0
- sweatstack-0.1.0/pyproject.toml +31 -0
- sweatstack-0.1.0/script.py +25 -0
- sweatstack-0.1.0/setup.py.old +26 -0
- sweatstack-0.1.0/src/python_library/__init__.py +2 -0
- sweatstack-0.1.0/sweatstack/__init__.py +1 -0
- sweatstack-0.1.0/sweatstack/cli.py +38 -0
- sweatstack-0.1.0/sweatstack/client.py +383 -0
- sweatstack-0.1.0/sweatstack/ipython_init.py +12 -0
- sweatstack-0.1.0/sweatstack/schemas.py +167 -0
- sweatstack-0.1.0/test2.py +91 -0
|
@@ -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 @@
|
|
|
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,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()=}")
|