STRAVA - RUNNING LOG

1. OVERVIEW

I've been using Strava for few years now and simultaneously I've been updating my running log on Google Sheets. I treat Google Sheets as my backup (you never know when your data disappear from Strava) and it's much more convenient to browser my activities using GS than Strava. Moreover when you have your own running log, you there is much more freedom in terms of what data you need and want to see. So to avoid double work I've written a script that uses Strava and Google Sheets APIs to transfer all data I want to my running log.

2. Prerequisites

The first step is to create a running log Google Sheet template. You can do it by accessing this link. Before we will be able to use the script, we have to set up two APIs - Google Sheets and Strava. How to do it?

  1. In regards of Google Sheets, I highly recommend you to follow this tutorial → "Working with Google Sheets" by Al Sweigart which is a part of free ebook "Automate The Boring Stuff" → Automate The Boring Stuff, chapter 14.
  2. To set up Strava API, you have to visit Strava Settings and create your API application. As a Authorization Callback Domain please set localhost.
    Once your API application is created, please install all required modules provided in the requirements file, which can be found on my GitHub. When the modules are installed and APIs are set up, you can proceed with the code:
    Show/Hide code
     1
    					2
    					3
    					4
    					5
    					6
    					7
    					8
    					9
    					10
    					11
    					12
    					13
    					14
    					15
    					16
    					17
    					18
    					19
    					20
    					21
    					22
    					23
    					24
    					25
    					26
    					27
    					28
    					29
    					30
    					31
    					32
    					33
    					34
    					35
    					36
    import gspread, requests, urllib3, time, os
    					from oauth2client.service_account import ServiceAccountCredentials
    					from pace_calulator import calculator
    					urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    					number = input("How many last activities would you like to update? ")
    					scopes = [
    						'https://www.googleapis.com/auth/spreadsheets',
    						'https://www.googleapis.com/auth/drive'
    					]
    					path = os.getcwd()
    					credentials_path = os.path.join(path, "credentials.json")
    
    					creds = ServiceAccountCredentials.from_json_keyfile_name(credentials_path, scopes = scopes)
    					file = gspread.authorize(creds)
    					workbook = file.open_by_key('xxxxxWorkBookKeyxxxxx')
    					sheet = workbook.worksheet('2023')
    
    					auth_url = "https://www.strava.com/oauth/token"
    					activites_url = "https://www.strava.com/api/v3/athlete/activities"
    					payload = {
    						'client_id': "97763",
    						'client_secret': 'XXXXXXXXXXXXXXXXXXX',
    						'refresh_token': 'XXXXXXXXXXXXXXXXXXX',
    						'grant_type': "refresh_token",
    						'f': 'json'
    					}
    					print("Connecting to Strava...")
    					res = requests.post(auth_url, data=payload, verify=False)
    					access_token = res.json()['access_token']
    					print("Access Token = {}.\n"
    						"Access granted.\n".format(access_token))
    
    
    					header_all = {'Authorization': 'Bearer ' + access_token}
    					param_all = {'per_page': number, 'page': 1}
    					print(f"Validating last {number} activities on Strava... ")
    					

    There are few lines which you have to adjust with your data:
    • workbook = file.open_by_key('xxxxxWorkBookKeyxxxxx') and sheet = workbook.worksheet ('2023') (lines 15 and 16) have to be filled in with your workbook key and worksheet name;
    • 'client_id': "97763", 'client_secret': 'XXXXXXXXXXXXXXXXXXX' and 'refresh_token': 'XXXXXXXXXXXXXXXXXXX' have to be filled with respective data form your Strava API application.

    The snippet presented above is responsible for API connection and getting a number (represented by a variable number) of running activities to store it in a list all_activities.
    We are also importing pace_calculator function from calculator module. It computes the pace from m/s to min/km. Calculator's code can be found below:
    Show/Hide code
    1
    				2
    				3
    				4
    				5
    				6
    				7
    def calculator(distance, total_time):
    					total_seconds = (int(total_time))
    					seconds_per_km = float(total_seconds) / (float(distance)/1000)
    
    					minutes_per_km = int(seconds_per_km / 60)
    					seconds_remainder = int(seconds_per_km - (minutes_per_km * 60))
    					return '{}:{:0=2d} min/km'.format(minutes_per_km, seconds_remainder)
    				

3. CODE

Let's have a look at the rest of the code.
Show/Hide code
1
			2
			3
			4
			5
activities = []
			for activity in all_activities:
				get_activity_url = "https://www.strava.com/api/v3/activities/{}".format(activity['id'])
				my_activity = requests.get(get_activity_url, headers=header).json()
				activities.append(my_activity)
				
We create an empty list activities. Then we loop through all running activities stored in all_activities (which was created in a step Prerequisites), we retrieve a link to each activity and then we store each activity (which is basically a dictionary) in the activities list (so we got a list of dictionaries).


Once we have all needed data stored in activities, we can proceed with sending it to Google Sheets. We start with creating a list all_dates and we fill it with all dates which are in the first column of our Google Sheets template. Then we go through every activity stored in the activities list, as every activity is a dictionary, we access all of the needed pieces of data and assign it to appropriate Google Sheets cell. Depending on the name of the activity, we assign a type of a training to it. When an activity is updated in Google Sheets successfully, we send a message to the user.

Show/Hide code
 1
			2
			3
			4
			5
			6
			7
			8
			9
			10
			11
			12
			13
			14
			15
			16
			17
			18
			19
			20
			21
			22
			23
			24
			25
			26
			27
			28
			29
			30
			31
			32
			33
			34
			35
			36
			37
			38
			39
			40
			41
			42
			43
			44
			45
			46
			47
			48
			49
			50
			51
			52
			53
			54
all_dates = sheet.col_values(1)

			for activity in activities:
				if activity["start_date_local"][:10] in all_dates:
					activity_date = activity["start_date_local"][:10]
					date = sheet.find(activity_date)
					activity_name = activity['name']
					print(activity_name, 'at', activity_date, 'found! \nUpdating...')
					sheet.update_cell(date.row, date.col+1, activity['name'])
					time.sleep(1)
					sheet.update_cell(date.row, date.col+2, activity['distance'])
					time.sleep(1)
					sheet.update_cell(date.row, date.col+3, time.strftime("%H:%M:%S", time.gmtime(activity['moving_time'])))
					time.sleep(1)
					sheet.update_cell(date.row, date.col+4, calculator(activity['distance'],activity['moving_time']))
					time.sleep(1)
					sheet.update_cell(date.row, date.col + 6, activity['total_elevation_gain'])
					time.sleep(1)
					sheet.update_cell(date.row, date.col + 7, activity['description'])
					time.sleep(3)
					if activity_name.startswith('Rozbieganie na bieżni'):
						sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
					elif activity_name.startswith('Rozbieganie'):
						sheet.update_cell(date.row, date.col + 8, "rozbieganie")
					elif activity_name.startswith('Siła biegowa - podbiegi'):
						sheet.update_cell(date.row, date.col + 8, "siła (podbiegi)")
					elif activity_name.startswith('Siła biegowa - skip'):
						sheet.update_cell(date.row, date.col + 8, "siła (skip)")
					elif activity_name.startswith('Siła biegowa - VK'):
						sheet.update_cell(date.row, date.col + 8, "siła (VK)")
					elif activity_name.startswith('Długie wybieganie'):
						sheet.update_cell(date.row, date.col + 8, "długie wybieganie")
					elif activity_name.startswith('Wycieczka biegowa'):
						sheet.update_cell(date.row, date.col + 8, "wycieczka biegowa")
					elif activity_name.startswith('Rozbieganie na bieżni'):
						sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
					elif activity_name.startswith('Zabawa biegowa'):
						sheet.update_cell(date.row, date.col + 8, activity_name[16:])
					elif activity_name.startswith('Drugi zakres na bieżni'):
						sheet.update_cell(date.row, date.col + 8, "drugi zakres (%)")
					elif activity_name.startswith('Drugi zakres -'):
						sheet.update_cell(date.row, date.col + 8, "drugi zakres")
					elif "x400 m" in activity_name:
						sheet.update_cell(date.row, date.col + 8, "400 m")
					elif "x800 m" in activity_name:
						sheet.update_cell(date.row, date.col + 8, "800 m")
					elif "x1 km" in activity_name:
						sheet.update_cell(date.row, date.col + 8, "1 km")
					elif "x200 m" in activity_name:
						sheet.update_cell(date.row, date.col + 8, "1 km")
					if activity['has_heartrate'] == True or False:
						sheet.update_cell(date.row, date.col + 5,
										f"{int(activity['average_heartrate'])} ({int(activity['max_heartrate'])})")
					print(activity['name'], 'updated!\n')
			


When all activities have been updated, scripts looks for empty Activity name cells, because it means it was a rest day. When an empty cell is found, the row is being merged and formatted as a rest day. When the script is done, it prints out a summary, how many training/rest days have been updated.

Show/Hide code
 1
			2
			3
			4
			5
			6
			7
			8
			9
			10
			11
			12
			13
			14
			15
			16
			17
			18
			19
			20
			21
all_activities_done = sheet.col_values(2)
			rest_days = 0
			print("\nUpdating rest days...\n")
			for index, val in enumerate(all_activities_done, start=1):
					if val == "":
						rest_days += 1
						sheet.merge_cells(f"B{index}:I{index}", merge_type='MERGE_ALL')
						time.sleep(1)
						sheet.update_cell(index, 2, 'WOLNE')
						time.sleep(1)
						sheet.format(f"B{index}:I{index}",{
							"backgroundColor": {
								"red": 0.65,
								"green": 0.65,
								"blue": 0.65
							},
							"horizontalAlignment": "CENTER",
						})
			print("---- UPDATE COMPLETED ----")
			print(f'{len(activities)} training day(s) updated!')
			print(rest_days, 'rest day(s) updated!')
			

4. Complete code

Complete code can be found below and on my GitHub and below.

Show/hide complete code
  1
				2
				3
				4
				5
				6
				7
				8
				9
				10
				11
				12
				13
				14
				15
				16
				17
				18
				19
				20
				21
				22
				23
				24
				25
				26
				27
				28
				29
				30
				31
				32
				33
				34
				35
				36
				37
				38
				39
				40
				41
				42
				43
				44
				45
				46
				47
				48
				49
				50
				51
				52
				53
				54
				55
				56
				57
				58
				59
				60
				61
				62
				63
				64
				65
				66
				67
				68
				69
				70
				71
				72
				73
				74
				75
				76
				77
				78
				79
				80
				81
				82
				83
				84
				85
				86
				87
				88
				89
				90
				91
				92
				93
				94
				95
				96
				97
				98
				99
				100
				101
				102
				103
				104
				105
				106
				107
				108
				109
				110
				111
				112
				113
				114
				115
				116
				117
				118
				119
				120
				121
				122
				123
				124
				125
				126
				127
				128
				129
				130
				131
				132
				133
				134
				135
				136
				137
				138
				139
				140
				141
				142
				143
				144
				145
				146
				147
				148
				149
#!/bin/bash
				"""
				contact - [email protected]
				Strava - https://www.strava.com/athletes/23952867
				version history 
				1.0 - 26/12/2022
				1.1 - 26/02/2023 - added 
				________________________
				Script uses Strava API to gather your running activities and sends them to Google Sheet running journal (template can
				be found here -> https://docs.google.com/spreadsheets/d/1j7l_FCBkn2b91tRHDA_iI0bXczwVnMg0p0RmMQwrH4I/copy.

				Script consists of two files. Script anc pace_calculator.py. Calculator is necessary to calculate the running pace.

				Provide script with an integer - how many last activities (non-running activities included) would you like to analyse
				and paste to the Google Sheet. Then script separates non-running activities from running activities and pastes your
				running data to Google sheet. The last step is to update the rest days. Script updates all rows with DATE cell empty.

				TODO list:
				1. Instead of "How many last activities would you like to update", user should be able to choose the starting date.
				"""


				#!/bin/bash
				import gspread, requests, urllib3, time, os
				from oauth2client.service_account import ServiceAccountCredentials
				from pace_calulator import calculator
				urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
				number = input("How many last activities would you like to update? ")
				scopes = [
					'https://www.googleapis.com/auth/spreadsheets',
					'https://www.googleapis.com/auth/drive'
				]
				path = os.getcwd()
				credentials_path = os.path.join(path, "credentials.json")

				creds = ServiceAccountCredentials.from_json_keyfile_name(credentials_path, scopes = scopes)
				file = gspread.authorize(creds)
				workbook = file.open_by_key('xxxxxWorkBookKeyxxxxx')
				sheet = workbook.worksheet('2023')

				auth_url = "https://www.strava.com/oauth/token"
				activites_url = "https://www.strava.com/api/v3/athlete/activities"
				payload = {
					'client_id': "97763",
					'client_secret': 'XXXXXXXXXXXXXXXXXXX',
					'refresh_token': 'XXXXXXXXXXXXXXXXXXX',
					'grant_type': "refresh_token",
					'f': 'json'
				}
				print("Connecting to Strava...")
				res = requests.post(auth_url, data=payload, verify=False)
				access_token = res.json()['access_token']
				print("Access Token = {}.\n"
					"Access granted.\n".format(access_token))


				header_all = {'Authorization': 'Bearer ' + access_token}
				param_all = {'per_page': number, 'page': 1}
				print(f"Validating last {number} activities on Strava... ")
				all_activities = []

				my_dataset = requests.get(activites_url, headers=header_all, params=param_all).json()
				for item in reversed(my_dataset):
					if item["sport_type"] == "Run":
						all_activities.append(item)
				print(f"Found {len(all_activities)} running activities!")

				res = requests.post(auth_url, data=payload, verify=False) # auth_url & payload referenced in code already above
				access_token = res.json()['access_token']
				header = {'Authorization': 'Bearer ' + access_token}
				activities = []
				for activity in all_activities:
					get_activity_url = "https://www.strava.com/api/v3/activities/{}".format(activity['id'])
					my_activity = requests.get(get_activity_url, headers=header).json()
					activities.append(my_activity)

				all_dates = sheet.col_values(1)

				for activity in activities:
					if activity["start_date_local"][:10] in all_dates:
						activity_date = activity["start_date_local"][:10]
						date = sheet.find(activity_date)
						activity_name = activity['name']
						print(activity_name, 'on', activity_date, 'found! \nUpdating...')
						sheet.update_cell(date.row, date.col+1, activity['name'])
						time.sleep(1)
						sheet.update_cell(date.row, date.col+2, activity['distance'])
						sheet.update_cell(date.row, date.col+3, time.strftime("%H:%M:%S", time.gmtime(activity['moving_time'])))
						time.sleep(1)
						sheet.update_cell(date.row, date.col+4, calculator(activity['distance'],activity['moving_time']))
						sheet.update_cell(date.row, date.col + 6, activity['total_elevation_gain'])
						sheet.update_cell(date.row, date.col + 7, activity['description'])
						time.sleep(1)
						if activity_name.startswith('Rozbieganie na bieżni'):
							sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
						elif activity_name.startswith('Rozbieganie'):
							sheet.update_cell(date.row, date.col + 8, "rozbieganie")
						elif activity_name.startswith('Siła biegowa - podbiegi'):
							sheet.update_cell(date.row, date.col + 8, "siła (podbiegi)")
						elif activity_name.startswith('Siła biegowa - skip'):
							sheet.update_cell(date.row, date.col + 8, "siła (skip)")
						elif activity_name.startswith('Siła biegowa - VK'):
							sheet.update_cell(date.row, date.col + 8, "siła (VK)")
						elif activity_name.startswith('Długie wybieganie'):
							sheet.update_cell(date.row, date.col + 8, "długie wybieganie")
						elif activity_name.startswith('Wycieczka biegowa'):
							sheet.update_cell(date.row, date.col + 8, "wycieczka biegowa")
						elif activity_name.startswith('Rozbieganie na bieżni'):
							sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
						elif activity_name.startswith('Zabawa biegowa'):
							sheet.update_cell(date.row, date.col + 8, activity_name[16:])
						elif activity_name.startswith('Drugi zakres na bieżni'):
							sheet.update_cell(date.row, date.col + 8, "drugi zakres (%)")
						elif activity_name.startswith('Drugi zakres -'):
							sheet.update_cell(date.row, date.col + 8, "drugi zakres")
						elif "x400 m" in activity_name:
							sheet.update_cell(date.row, date.col + 8, "400 m")
						elif "x800 m" in activity_name:
							sheet.update_cell(date.row, date.col + 8, "800 m")
						elif "x1 km" in activity_name:
							sheet.update_cell(date.row, date.col + 8, "1 km")
						elif "x200 m" in activity_name:
							sheet.update_cell(date.row, date.col + 8, "1 km")
						if activity['has_heartrate'] == True or False:
							sheet.update_cell(date.row, date.col + 5,
											f"{int(activity['average_heartrate'])} ({int(activity['max_heartrate'])})")
						print(activity['name'], 'updated!\n')

				all_activities_done = sheet.col_values(2)
				rest_days = 0
				print("\nUpdating rest days...\n")
				for index, val in enumerate(all_activities_done, start=1):
						if val == "":
							rest_days += 1
							sheet.merge_cells(f"B{index}:I{index}", merge_type='MERGE_ALL')
							time.sleep(1)
							sheet.update_cell(index, 2, 'WOLNE')
							time.sleep(1)
							sheet.format(f"B{index}:I{index}",{
								"backgroundColor": {
									"red": 0.65,
									"green": 0.65,
									"blue": 0.65
								},
								"horizontalAlignment": "CENTER",
							})
				print("---- UPDATE COMPLETED ----")
				print(f'{len(activities)} training day(s) updated!')
				print(rest_days, 'rest day(s) updated!')
				

5. Demo

6. Version history

version 1.0 - 26/12/2022
version 1.1 - 26/02/2023 - rest day detection added
version 1.2 - 27/02/2023 - rest day counter added