Created
October 17, 2021 08:27
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 = "[email protected]" | |
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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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!