Gireg commited on
Commit
21d674f
1 Parent(s): e5d1066

first commit

Browse files
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .env
2
+ .idea
main.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import altair as alt
2
+ import streamlit as st
3
+ import pandas as pd
4
+
5
+ import strava
6
+ from utils import find_default_publish_start_end_date
7
+
8
+ st.set_page_config(
9
+ page_title="Streamlit Activities analysis for Strava",
10
+ page_icon=":safety_pin:",
11
+ )
12
+
13
+ strava_header = strava.header()
14
+
15
+ st.header(":safety_pin: Streamlit Strava activities analysis")
16
+
17
+ strava_auth = strava.authenticate(header=strava_header, stop_if_unauthenticated=False)
18
+
19
+ if strava_auth is None:
20
+ st.markdown("Click the \"Connect with Strava\" button at the top to login with your Strava account and get started.")
21
+ st.stop()
22
+
23
+ # analysis on shoes
24
+ st.divider()
25
+ st.header("Display shoes analysis")
26
+ if 'athlete' not in st.session_state:
27
+ athlete = strava.get_athlete_detail(strava_auth)
28
+ st.session_state.athlete = athlete
29
+
30
+ if 'dict_shoes' not in st.session_state:
31
+ shoes = strava.get_shoes(st.session_state.athlete)
32
+ dict_shoes = {shoe["name"]: shoe["converted_distance"] for shoe in shoes}
33
+ st.session_state.dict_shoes = dict_shoes
34
+
35
+ all_shoes_names = st.session_state.dict_shoes.keys()
36
+
37
+ selected_shoes = st.multiselect(
38
+ label="Select columns to plot",
39
+ options=all_shoes_names,
40
+ )
41
+
42
+ distances = [st.session_state.dict_shoes[shoe_name] for shoe_name in selected_shoes]
43
+ if selected_shoes:
44
+ chart_data = pd.DataFrame({
45
+ 'index':selected_shoes,
46
+ 'kilometers':distances
47
+ })
48
+ st.bar_chart(chart_data)
49
+ else:
50
+ st.write("No column(s) selected")
51
+
52
+ # get activities on a period
53
+ st.divider()
54
+ st.header("Display zones on a period")
55
+
56
+ default_start_date, default_end_date = find_default_publish_start_end_date()
57
+
58
+ col_start_date, col_end_date = st.columns(2)
59
+ with col_start_date:
60
+ st.date_input("Start date", value=default_start_date, key="start_date")
61
+ with col_end_date:
62
+ st.date_input("End date", value=default_end_date, key="end_date")
63
+ if st.session_state.start_date > st.session_state.end_date:
64
+ st.error("Error: End date must fall after start date.")
65
+ else:
66
+ st.session_state.activities = strava.get_activities_on_period(strava_auth, [], st.session_state.start_date, st.session_state.end_date, 1)
67
+
68
+ st.write(f"You got {len(st.session_state.activities)} activities")
69
+
70
+ activities_zones = {}
71
+ for activity in st.session_state.activities:
72
+ if not activity["has_heartrate"]:
73
+ continue
74
+ try:
75
+ st.session_state.activity_zones = strava.get_activity_zones(strava_auth, activity["id"])[0]["distribution_buckets"]
76
+ except Exception as e:
77
+ st.write(e)
78
+ if not activities_zones:
79
+ activities_zones = {idx: zone["time"] // 60 for idx, zone in enumerate(st.session_state.activity_zones)}
80
+ else:
81
+ for idx, zone in enumerate(st.session_state.activity_zones):
82
+ activities_zones[idx] += (zone["time"] // 60)
83
+
84
+ zones_label = ["zone 1", "zone 2", "zone 3", "zone 4", "zone 5"]
85
+ zones_df = pd.DataFrame({
86
+ 'zones': zones_label,
87
+ 'minutes': activities_zones.values()
88
+ })
89
+
90
+ scale = alt.Scale(
91
+ domain=zones_label,
92
+ range=["#008000", "#ffcf3e", "#f67200", "#ee1010", "#3f2204"],
93
+ )
94
+ color = alt.Color("zones:N", scale=scale)
95
+
96
+ bars = (
97
+ alt.Chart(zones_df)
98
+ .mark_bar()
99
+ .encode(
100
+ x="zones",
101
+ y="minutes",
102
+ color=color,
103
+ )
104
+ )
105
+
106
+ st.altair_chart(bars, theme="streamlit", use_container_width=True)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit==1.26.0
2
+ httpx==0.24.1
3
+ pandas==2.1.0
4
+ python-dotenv==1.0.0
static/api_logo_pwrd_by_strava.png ADDED
static/btn_strava_connect.png ADDED
strava.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+
4
+ import httpx
5
+ import streamlit as st
6
+ import webbrowser
7
+ from dotenv import load_dotenv
8
+ from datetime import datetime
9
+ load_dotenv()
10
+
11
+ APP_URL = os.environ["APP_URL"]
12
+ STRAVA_CLIENT_ID = os.environ["STRAVA_CLIENT_ID"]
13
+ STRAVA_CLIENT_SECRET = os.environ["STRAVA_CLIENT_SECRET"]
14
+ STRAVA_AUTHORIZATION_URL = "https://www.strava.com/oauth/authorize"
15
+ STRAVA_API_BASE_URL = "https://www.strava.com/api/v3"
16
+ SCOPE = "activity:read_all,profile:read_all,activity:write"
17
+ DEFAULT_ACTIVITY_LABEL = "NO_ACTIVITY_SELECTED"
18
+ STRAVA_ORANGE = "#fc4c02"
19
+
20
+
21
+
22
+ @st.cache_data
23
+ def load_image_as_base64(image_path):
24
+ with open(image_path, "rb") as f:
25
+ contents = f.read()
26
+ return base64.b64encode(contents).decode("utf-8")
27
+
28
+
29
+ def powered_by_strava_logo():
30
+ base64_image = load_image_as_base64("static/api_logo_pwrd_by_strava.png")
31
+ st.markdown(
32
+ f'<img src="data:image/png;base64,{base64_image}" width="100%" alt="powered by strava">',
33
+ unsafe_allow_html=True,
34
+ )
35
+
36
+ @st.cache_data
37
+ def authorization_url():
38
+ request = httpx.Request(
39
+ method="GET",
40
+ url=STRAVA_AUTHORIZATION_URL,
41
+ params={
42
+ "client_id": STRAVA_CLIENT_ID,
43
+ "redirect_uri": APP_URL,
44
+ "response_type": "code",
45
+ "approval_prompt": "auto",
46
+ "scope": SCOPE
47
+ }
48
+ )
49
+
50
+ return request.url
51
+
52
+
53
+ def login_header(header=None):
54
+ strava_authorization_url = authorization_url()
55
+
56
+ if header is None:
57
+ base = st
58
+ else:
59
+ col1, _, _, button = header
60
+ base = button
61
+
62
+ with col1:
63
+ powered_by_strava_logo()
64
+
65
+ base64_image = load_image_as_base64("./static/btn_strava_connect.png")
66
+ base.markdown(
67
+ (
68
+ f"<a href=\"{strava_authorization_url}\">"
69
+ f" <img alt=\"strava login\" src=\"data:image/png;base64,{base64_image}\" width=\"100%\">"
70
+ f"</a>"
71
+ ),
72
+ unsafe_allow_html=True,
73
+ )
74
+
75
+
76
+ def logout_header(header=None):
77
+ if header is None:
78
+ base = st
79
+ else:
80
+ _, col2, _, button = header
81
+ base = button
82
+
83
+
84
+ with col2:
85
+ powered_by_strava_logo()
86
+
87
+ if base.button("Log out"):
88
+ webbrowser.open(APP_URL, new=0)
89
+
90
+
91
+ def logged_in_title(strava_auth, header=None):
92
+ if header is None:
93
+ base = st
94
+ else:
95
+ col, _, _, _ = header
96
+ base = col
97
+
98
+ first_name = strava_auth["athlete"]["firstname"]
99
+ last_name = strava_auth["athlete"]["lastname"]
100
+ col.markdown(f"*Welcome, {first_name} {last_name}!*")
101
+
102
+
103
+ @st.cache_data
104
+ def exchange_authorization_code(authorization_code):
105
+ response = httpx.post(
106
+ url="https://www.strava.com/oauth/token",
107
+ json={
108
+ "client_id": STRAVA_CLIENT_ID,
109
+ "client_secret": STRAVA_CLIENT_SECRET,
110
+ "code": authorization_code,
111
+ "grant_type": "authorization_code",
112
+ }
113
+ )
114
+ try:
115
+ response.raise_for_status()
116
+ except httpx.HTTPStatusError:
117
+ st.error("Something went wrong while authenticating with Strava. Please reload and try again")
118
+ st.experimental_set_query_params()
119
+ st.stop()
120
+ return
121
+
122
+ strava_auth = response.json()
123
+
124
+ return strava_auth
125
+
126
+ def authenticate(header=None, stop_if_unauthenticated=True):
127
+ query_params = st.experimental_get_query_params()
128
+ authorization_code = query_params.get("code", [None])[0]
129
+
130
+ if authorization_code is None:
131
+ authorization_code = query_params.get("session", [None])[0]
132
+
133
+ if authorization_code is None:
134
+ login_header(header=header)
135
+ if stop_if_unauthenticated:
136
+ st.stop()
137
+ return
138
+ else:
139
+ logout_header(header=header)
140
+ strava_auth = exchange_authorization_code(authorization_code)
141
+ logged_in_title(strava_auth, header)
142
+ st.experimental_set_query_params(session=authorization_code)
143
+
144
+ return strava_auth
145
+
146
+
147
+ def header():
148
+ col1, col2, col3 = st.columns(3)
149
+
150
+ with col3:
151
+ strava_button = st.empty()
152
+
153
+ return col1, col2, col3, strava_button
154
+
155
+ def catch_strava_api_error(response):
156
+ if response.status_code == 200:
157
+ return
158
+ st.write(response.status_code)
159
+ if response.status_code == 401:
160
+ st.error("You are not authorized to access this resource. Please relog yourself.")
161
+ st.stop()
162
+ return
163
+ else:
164
+ st.error(response)
165
+ if response["errors"]:
166
+ st.error(f"Something went wrong while fetching data from Strava. Please reload and try again (error message: {response['message']})")
167
+ st.stop()
168
+ return
169
+
170
+ def strava_call(auth, uri_params, params = None):
171
+ access_token = auth["access_token"]
172
+ response = httpx.get(
173
+ url=f"{STRAVA_API_BASE_URL}/{uri_params}",
174
+ params=params,
175
+ headers={
176
+ "Authorization": f"Bearer {access_token}",
177
+ },
178
+ )
179
+ catch_strava_api_error(response)
180
+ return response.json()
181
+
182
+ @st.cache_data
183
+ def get_athlete_detail(auth, page=1):
184
+ return strava_call(auth, "athlete")
185
+
186
+ @st.cache_data
187
+ def get_shoes(athlete):
188
+ return athlete["shoes"]
189
+
190
+ @st.cache_data
191
+ def get_activity(activity_id, auth):
192
+ return strava_call(auth, f"activities/{activity_id}")
193
+
194
+ @st.cache_data
195
+ def get_activities_on_period(auth, activities, start_date, end_date, page):
196
+ response = get_activities(auth, page)
197
+ number_added = 0
198
+ for activity in response:
199
+ strava_start_date = datetime.strptime(activity["start_date"], '%Y-%m-%dT%H:%M:%SZ').date()
200
+ if strava_start_date >= start_date and strava_start_date <= end_date:
201
+ activities.append(activity)
202
+ number_added += 1
203
+ if number_added == 0:
204
+ return activities
205
+ else:
206
+ return get_activities_on_period(auth, activities, start_date, end_date, page + 1)
207
+
208
+ @st.cache_data
209
+ def get_activities(auth, page=1):
210
+ return strava_call(auth, f"athlete/activities", params={"page": page})
211
+
212
+ @st.cache_data
213
+ def get_activity_zones(auth, activity_id):
214
+ return strava_call(auth, f"activities/{activity_id}/zones")
215
+
216
+ @st.cache_data
217
+ def get_athlete_zones(auth):
218
+ return strava_call(auth, f"athlete/zones")
219
+
220
+ # @st.cache_data
221
+ # def get_all_activities(auth):
222
+ # page = 1
223
+ # activities = []
224
+ # while True:
225
+ # new_activities = strava_call(auth, f"athlete/activities", params={
226
+ # "page": page,
227
+ # "per_page": 200
228
+ # })
229
+ # activities.append(new_activities)
230
+ # if len(new_activities) == 0:
231
+ # break
232
+ # page += 1
233
+ #
234
+ # return activities
235
+
236
+ # def export_all_activities(auth):
237
+ # activities = get_all_activities(auth=auth)
238
+ # st.write(f"You got {len(activities)} activities")
239
+ # with open('activities.json', 'w') as f:
240
+ # json.dump(activities, f)
utils.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ import streamlit as st
3
+
4
+ @st.cache_data
5
+ def find_default_publish_start_end_date():
6
+ today = datetime.now()
7
+ start = today - timedelta(days=today.weekday())
8
+ end = start + timedelta(days=6)
9
+ if end > today:
10
+ end = today
11
+ return start, end