Skip to content

Instantly share code, notes, and snippets.

@oscarsan
Created October 17, 2021 08:27
Show Gist options
  • Save oscarsan/5a31185a364d29744e275070d643c307 to your computer and use it in GitHub Desktop.
Save oscarsan/5a31185a364d29744e275070d643c307 to your computer and use it in GitHub Desktop.
Python 3 scripts export whoop workout data between 2 dates and creates TCX file for importing to trainingPeaks
#!/usr/bin/env python3
import requests # for getting URL
import json # for parsing json
from datetime import datetime # datetime parsing
import pytz # timezone adjusting
import xmlschema
#################################################################
# USER VARIABLES
username = "username@gmail.com"
password = "password"
save_directory = "./" # keep trailing slash
tcx_schema = xmlschema.XMLSchema('https://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd')
#################################################################
# GET ACCESS TOKEN
# Post credentials
r = requests.post("https://api-7.whoop.com/oauth/token", json={
"grant_type": "password",
"issueRefresh": False,
"password": password,
"username": username
})
# Exit if fail
if r.status_code != 200:
print("Fail - Credentials rejected.")
exit()
else:
print("Success - Credentials accepted")
# Set userid/token variables
userid = r.json()['user']['id']
access_token = r.json()['access_token']
#################################################################
# GET DATA
# Download data
url = 'https://api-7.whoop.com/users/{}/cycles'.format(userid)
params = {
'start': '2021-10-12T00:00:00.000Z',
'end': '2021-10-12T23:00:00.000Z'
}
headers = {
'Authorization': 'bearer {}'.format(access_token)
}
r = requests.get(url, params=params, headers=headers)
# Check if user/auth are accepted
if r.status_code != 200:
print("Fail - User ID / auth token rejected.")
exit()
else:
print("Success - User ID / auth token accepted")
#################################################################
# PARSE/TRANSFORM DATA
# Convert data to json
data_raw = r.json()
print(json.dumps(data_raw, indent=2))
def get_workout(workout_id):
# Download data
url = f'https://api-7.whoop.com/users/{userid}/workouts/{workout_id}'
headers = {
'Authorization': 'bearer {}'.format(access_token)
}
r = requests.get(url, params=params, headers=headers)
# Check if user/auth are accepted
if r.status_code != 200:
print("Fail - User ID / auth token rejected.")
return {}
else:
print("Success - User ID / auth token accepted")
return r.json()
def get_hr(start_time, end_time, step):
# Download data
url = f'https://api-7.whoop.com/users/{userid}/metrics/heart_rate'
headers = {
'Authorization': 'bearer {}'.format(access_token)
}
params = {
'start': start_time,
'end': end_time,
'step': step
}
r = requests.get(url, params=params, headers=headers)
# Check if user/auth are accepted
if r.status_code != 200:
print("Fail - User ID / auth token rejected.")
return {}
else:
print("Success - User ID / auth token accepted")
return r.json()
def write_tcx(tcx_training, training_name):
with open(save_directory + '{}.tcx'.format(training_name), 'w') as outfile:
outfile.write(tcx_training)
print("Success - tcx data saved.")
# Make data object
data_summary = []
# Iterate through data
for d in data_raw:
if d['strain']:
# Append record to data dictionary
for workout in d['strain']['workouts']:
theworkout = get_workout(workout['id'])
data_summary.append(theworkout)
txcfiles = []
for workout in data_summary:
hts = get_hr(workout['during']['lower'], workout['during']['upper'], 6)
lower = datetime.strptime(workout['during']['lower'], '%Y-%m-%dT%H:%M:%S.%fZ')
upper = datetime.strptime(workout['during']['upper'], '%Y-%m-%dT%H:%M:%S.%fZ')
total = upper - lower
calories = workout['kilojoules'] * 0.239
lapStart = datetime.fromtimestamp(hts['values'][0]['time'] / 1000.0).strftime('%Y-%m-%dT%H:%M:%SZ')
tcxfile = """<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<TrainingCenterDatabase xmlns="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation
="http://www.garmin.com/xmlschemas/ActivityExtension/v2 http://www.garmin.com/xmlschemas/ActivityExtensionv2.xsd http://www.garmin.com/xmlschemas/TrainingCenterDat
abase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd">
<Activities>
<Activity Sport="Other">
<Id>{}</Id>
<Lap StartTime="{}">
<TotalTimeSeconds>{}.{}</TotalTimeSeconds>
<DistanceMeters>0</DistanceMeters>
<Calories>{}</Calories>
<AverageHeartRateBpm xsi:type="HeartRateInBeatsPerMinute_t">
<Value>{}</Value>
</AverageHeartRateBpm>
<MaximumHeartRateBpm xsi:type="HeartRateInBeatsPerMinute_t">
<Value>{}</Value>
</MaximumHeartRateBpm>
<Intensity>Active</Intensity>
<TriggerMethod>HeartRate</TriggerMethod>
<Track>""".format(lapStart, lapStart, total.seconds, total.microseconds, int(calories),
workout['averageHeartRate'],
workout['maxHeartRate'])
for hrs in hts['values']:
time = datetime.fromtimestamp(hrs['time'] / 1000.0).strftime('%Y-%m-%dT%H:%M:%SZ')
hr = hrs['data']
tcxfile = tcxfile + """
<Trackpoint>
<Time>{}</Time>
<HeartRateBpm xsi:type="HeartRateInBeatsPerMinute_t">
<Value>{}</Value>
</HeartRateBpm>
</Trackpoint>""".format(time, hr)
tcxfile = tcxfile + """
</Track>
</Lap>
</Activity>
</Activities>
</TrainingCenterDatabase>
"""
if tcx_schema.is_valid(tcxfile):
write_tcx(tcxfile, lapStart)
else:
print(tcx_schema.validate(tcxfile))
exit()
@jaredc2
Copy link

jaredc2 commented Apr 17, 2022

Sorry i now this is worded poorly, but you can get all your data. and they should help with your scripts.

Please report This, . If someone wants to clean up my wording, and standaized this, please do. But the more people that make this request, they should be able to automate it.

Will everyone please contact them and request it, and get them to speed up their process.

Below is what they send me, and i requested 45 days free membership for the delay.
Please also explained the historical SPO2 data they can not provide is the most important data you need. And request a 45 day free membership

This is how I requested the data

I called and explained that the following all access to download your personal Data.

Google has it. Called Take outhttps://takeout.google.com/?pli=1

Instagram

Twitter

And I told them the other health apps have it.

Samsung

Apple

Fitbit

And i told them under my state and federal laws their required to provide all data releated to my health, and they i would submit a open records request, and medical records request, which their required to release under hippa.

and they i would send those forms next if required.You can also remind them they applied to be medical apporved, which means they must follow hippa laws.Note: I am sure theirs some other reasons that can be added.

they asked a reason, I would just like a option to download all data stored on whoop on me, and check for my medical conditions.

They then opened a support ticket, and i sent in the same info.

this was their response.

Thanks for reaching out, and thank you for your patience while we responded back to your inquiry. My name is Jillian, and I work on the Data Privacy Team here at WHOOP. We take the privacy and security of our Members' data very seriously.

We do have a process that allows you to export your WHOOP data. Our data export currently generates up to four excel spreadsheets of data. The first is a "metrics" data sheet that includes a daily log of heart rate data collected by your strap throughout the day. The second is a "recovery" data sheet that includes: RHR, HRV, Recovery, and detailed sleep information. The third is a "workouts" data sheet that, to the extent, you have logged workout activities, includes: Workout Strain, Day Strain, Sport, Max HR, Average HR, and calories. The fourth is a "journal" data sheet that, to the extent, you have logged journal responses, will include responses and associated notes. We have recently added skin temperature to our exports; however, we do not currently have historical SPO2. We are working with our internal team to have these added.

In order to export your data, we first need to validate your account. Could you please use the following link to log-in and submit a “Download Request”? https://app.whoop.com/settings/data-management

Below is what they send me, and i requested 45 days free membership for the delay.

Thank you for completing the user verification and submitting your data export request. The reason why the link is not available on our website is because we have a designated team who generates these data exports on the backend. I apologize for any inconvenience that this may cause.

We will aim to fulfill your request within the next 45 days, but we may need additional time depending on how complex your request is for us to complete and the number of requests we receive.

If we need additional time we will inform you within 45 days after we receive your request. Please let us know if you have any additional questions or concerns in the meantime!

Thank you for completing the user verification and submitting your data export request. The reason why the link is not available on our website is because we have a designated team who generates these data exports on the backend. I apologize for any inconvenience that this may cause.

We will aim to fulfill your request within the next 45 days, but we may need additional time depending on how complex your request is for us to complete and the number of requests we receive.

If we need additional time we will inform you within 45 days after we receive your request. Please let us know if you have any additional questions or concerns in the meantime!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment