Private
Public Access
1
0

doing trainingpeaks, untested

This commit is contained in:
Sander Roosendaal
2023-02-15 19:25:45 +01:00
parent 8912ee3c8e
commit 3eaef9ae70
12 changed files with 230 additions and 320 deletions

View File

@@ -3,3 +3,5 @@ from .strava import StravaIntegration
from .nk import NKIntegration from .nk import NKIntegration
from .sporttracks import SportTracksIntegration from .sporttracks import SportTracksIntegration
from .rp3 import RP3Integration from .rp3 import RP3Integration
from .trainingpeaks import TPIntegration

View File

@@ -244,8 +244,4 @@ class RP3Integration(SyncIntegration):
return super(RP3Integration, self).token_refresh(*args, **kwargs) return super(RP3Integration, self).token_refresh(*args, **kwargs)
# just as a quick test during development
u = User.objects.get(id=1)
integration_1 = RP3Integration(u)

View File

@@ -0,0 +1,134 @@
from .integrations import SyncIntegration, NoTokenError
from rowers.models import User, Rower, Workout, TombStone
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('high')
from rowers.utils import myqueue, dologging, myqueue
import requests
from rowingdata import rowingdata
from rowers.rower_rules import is_workout_user
import time
from django_rq import job
from rowers.tasks import check_tp_workout_id, handle_workout_tp_upload
from rowsandall_app.settings import (
TP_CLIENT_ID, TP_CLIENT_SECRET,
TP_REDIRECT_URI, TP_CLIENT_KEY,TP_API_LOCATION,
TP_OAUTH_LOCATION,
)
import gzip
import base64
from io import BytesIO
tpapilocation = TP_API_LOCATION
class TPIntegration(SyncIntegration):
def __init__(self, *args, **kwargs):
super(TPIntegration, self).__init__(*args, **kwargs)
self.oauth_data = {
'client_id': TP_CLIENT_ID,
'client_secret': TP_CLIENT_SECRET,
'redirect_uri': TP_REDIRECT_URI,
'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?",
'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'tptoken',
'refreshtokenname': 'tprefreshtoken',
'expirydatename': 'tptokenexpirydate',
'bearer_auth': False,
'base_url': "https://oauth.trainingpeaks.com/oauth/token",
'scope': 'write',
}
def createworkoutdata(self, w, *args, **kwargs):
filename = w.csvfilename
row = rowingdata(csvfile=filename)
tcxfilename = filename[:-4]+'.tcx'
try:
newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
except TypeError:
newnotes = 'from '+w.workoutsource+' via rowsandall.com'
row.exporttotcx(tcxfilename, notes=newnotes)
return tcxfilename
def workout_export(self, workout, *args, **kwargs) -> str:
thetoken = self.open()
tcxfilename = self.createworkoutdata(workout)
job = myqueue(
queue,
handle_workout_tp_upload,
workout,
thetoken,
tcxfilename
)
return job.id
def get_workouts(self, *args, **kwargs) -> int:
raise NotImplementedError("not implemented")
def get_workout(self, id) -> int:
raise NotImplementedError("not implemented")
def get_workout_list(self, *args, **kwargs) -> list:
raise NotImplementedError("not implemented")
def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover
return super(TPIntegration, self).make_authorization_url(self, *args, **kwargs)
def get_token(self, code, *args, **kwargs) -> (str, int, str):
# client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
post_data = {
"client_id": TP_CLIENT_KEY,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": TP_REDIRECT_URI,
"client_secret": TP_CLIENT_SECRET,
}
response = requests.post(
TP_OAUTH_LOCATION+"/oauth/token/",
data=post_data, verify=False,
)
if response.status_code != 200:
raise NoTokenError
try:
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except KeyError: # pragma: no cover
thetoken = ""
expires_in = 0
refresh_token = ""
return thetoken, expires_in, refresh_token
def open(self, *args, **kwargs) -> str:
return super(TPIntegration, self).open(*args, **kwargs)
def token_refresh(self, *args, **kwargs) -> str:
return super(TPIntegration, self).token_refresh(*args, **kwargs)
# just as a quick test during development
u = User.objects.get(id=1)
integration_1 = TPIntegration(u)

View File

@@ -47,6 +47,8 @@ import sys
import json import json
import traceback import traceback
from time import strftime from time import strftime
import base64
from io import BytesIO
from scipy import optimize from scipy import optimize
from scipy.signal import savgol_filter from scipy.signal import savgol_filter
@@ -105,7 +107,8 @@ except KeyError: # pragma: no cover
NK_API_LOCATION = CFG["nk_api_location"] NK_API_LOCATION = CFG["nk_api_location"]
TP_CLIENT_ID = CFG["tp_client_id"] TP_CLIENT_ID = CFG["tp_client_id"]
TP_CLIENT_SECRET = CFG["tp_client_secret"] TP_CLIENT_SECRET = CFG["tp_client_secret"]
TP_API_LOCATION = CFG["tp_api_location"]
tpapilocation = TP_API_LOCATION
from requests_oauthlib import OAuth1, OAuth1Session from requests_oauthlib import OAuth1, OAuth1Session
@@ -344,6 +347,46 @@ def handle_add_workouts_team(ws, t, debug=False, **kwargs):
return 1 return 1
def uploadactivity(access_token, filename, description='',
name='Rowsandall.com workout'):
data_gz = BytesIO()
with open(filename, 'rb') as inF:
s = inF.read()
with gzip.GzipFile(fileobj=data_gz, mode="w") as gzf:
gzf.write(s)
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer %s' % access_token
}
data = {
"UploadClient": "rowsandall",
"Filename": filename,
"SetWorkoutPublic": True,
"Title": name,
"Type": "rowing",
"Comment": description,
"Data": base64.b64encode(data_gz.getvalue()).decode("ascii")
}
resp = requests.post(tpapilocation+"/v3/file",
data=json.dumps(data),
headers=headers, verify=False)
if resp.status_code not in (200, 202): # pragma: no cover
dologging('tp_export.log',resp.status_code)
dologging('tp_export.log',resp.reason)
dologging('tp_export.log',json.dumps(data))
return 0, resp.reason, resp.status_code, headers
else:
return 1, "ok", 200, resp.headers
return 0, 0, 0, 0 # pragma: no cover
@app.task @app.task
def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs): def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs):
authorizationstring = str('Bearer ' + workout.user.tptoken) authorizationstring = str('Bearer ' + workout.user.tptoken)
@@ -361,6 +404,37 @@ def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs):
return 1 return 1
@app.task
def handle_workout_tp_upload(w, thetoken, tcxfilename, debug=False, **kwargs):
tpid = 0
r = w.user
if not tcxfilename:
return 0
res, reason, status_code, headers = uploadactivity(
thetoken, tcxfilename,
name=w.name
)
if res == 0:
w.tpid = -1
try:
os.remove(tcxfilename)
except WindowsError:
pass
w.save()
return 0
w.uploadedtotp = res
tpid = res
w.save()
os.remove(tcxfilename)
check_tp_workout_id(w,headers['Location'])
return tpid
@app.task @app.task
def instroke_static(w, metric, debug=False, **kwargs): def instroke_static(w, metric, debug=False, **kwargs):
f1 = w.csvfilename[6:-4] f1 = w.csvfilename[6:-4]

View File

@@ -234,10 +234,6 @@
<a href="/rowers/me/rp3authorize"> <a href="/rowers/me/rp3authorize">
Connect to RP3 Connect to RP3
</a> </a>
{% else %}
<a href="https://rp3rowing-app.com/home">
RP3
</a>
{% endif %} {% endif %}
</li> </li>
<li id="export-csv"> <li id="export-csv">

View File

@@ -1373,15 +1373,15 @@ class TPObjects(DjangoTestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@patch('rowers.tpstuff.requests.post', side_effect=mocked_requests) @patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests)
def test_tp_token_refresh(self, mock_post): def test_tp_token_refresh(self, mock_post):
response = self.c.get('/rowers/me/tprefresh/',follow=True) response = self.c.get('/rowers/me/tprefresh/',follow=True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@patch('rowers.tpstuff.requests.post', side_effect=mocked_requests) @patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests)
@patch('rowers.tpstuff.requests.get', side_effect=mocked_requests) @patch('rowers.integrations.trainingpeaks.requests.get', side_effect=mocked_requests)
def test_tp_upload(self, mock_get, mock_post): def test_tp_upload(self, mock_get, mock_post):
url = '/rowers/workout/'+encoded1+'/tpuploadw/' url = '/rowers/workout/'+encoded1+'/tpuploadw/'

Binary file not shown.

View File

@@ -1,232 +0,0 @@
from celery import Celery, app
from rowers.rower_rules import is_workout_user
import time
from django_rq import job
# All the functionality needed to connect to Runkeeper
from rowers.imports import *
from rowers.utils import dologging
from rowers.tasks import check_tp_workout_id
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')
from rowers.utils import myqueue
# Python
import gzip
import base64
from io import BytesIO
from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
TP_CLIENT_ID, TP_CLIENT_SECRET,
TP_REDIRECT_URI, TP_CLIENT_KEY,TP_API_LOCATION,
TP_OAUTH_LOCATION,
)
tpapilocation = TP_API_LOCATION
oauth_data = {
'client_id': TP_CLIENT_ID,
'client_secret': TP_CLIENT_SECRET,
'redirect_uri': TP_REDIRECT_URI,
'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?",
'content_type': 'application/x-www-form-urlencoded',
# 'content_type': 'application/json',
'tokenname': 'tptoken',
'refreshtokenname': 'tprefreshtoken',
'expirydatename': 'tptokenexpirydate',
'bearer_auth': False,
'base_url': "https://oauth.trainingpeaks.com/oauth/token",
'scope': 'write',
}
# Checks if user has UnderArmour token, renews them if they are expired
def tp_open(user):
return imports_open(user, oauth_data)
# Refresh ST token using refresh token
def do_refresh_token(refreshtoken):
return imports_do_refresh_token(refreshtoken, oauth_data)
# Exchange access code for long-lived access token
def get_token(code):
# client_auth = requests.auth.HTTPBasicAuth(TP_CLIENT_KEY, TP_CLIENT_SECRET)
post_data = {
"client_id": TP_CLIENT_KEY,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": TP_REDIRECT_URI,
"client_secret": TP_CLIENT_SECRET,
}
response = requests.post(
TP_OAUTH_LOCATION+"/oauth/token/",
data=post_data, verify=False,
)
if response.status_code != 200:
return 0,0,0
try:
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except KeyError: # pragma: no cover
thetoken = 0
expires_in = 0
refresh_token = 0
return thetoken, expires_in, refresh_token
# Make authorization URL including random string
def make_authorization_url(request): # pragma: no cover
return imports_make_authorization_url(oauth_data)
def getidfromresponse(response): # pragma: no cover
t = json.loads(response.text)
links = t["_links"]
id = links["self"][0]["id"]
return int(id)
def createtpworkoutdata(w):
filename = w.csvfilename
row = rowingdata(csvfile=filename)
tcxfilename = filename[:-4]+'.tcx'
try:
newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com'
except TypeError:
newnotes = 'from '+w.workoutsource+' via rowsandall.com'
row.exporttotcx(tcxfilename, notes=newnotes)
return tcxfilename
def tp_check(access_token): # pragma: no cover
headers = {
"Content-Type": "application/json",
'Accept': 'application/json',
'authorization': 'Bearer %s' % access_token
}
resp = requests.post(tpapilocation+"/v2/info/version",
headers=headers, verify=False)
return resp
def uploadactivity(access_token, filename, description='',
name='Rowsandall.com workout'):
data_gz = BytesIO()
with open(filename, 'rb') as inF:
s = inF.read()
with gzip.GzipFile(fileobj=data_gz, mode="w") as gzf:
gzf.write(s)
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer %s' % access_token
}
data = {
"UploadClient": "rowsandall",
"Filename": filename,
"SetWorkoutPublic": True,
"Title": name,
"Type": "rowing",
"Comment": description,
"Data": base64.b64encode(data_gz.getvalue()).decode("ascii")
}
#resp = requests.post(tpapilocation+"/v2/file/synchronous",
# data=json.dumps(data),
# headers=headers, verify=False)
resp = requests.post(tpapilocation+"/v3/file",
data=json.dumps(data),
headers=headers, verify=False)
if resp.status_code not in (200, 202): # pragma: no cover
dologging('tp_export.log',resp.status_code)
dologging('tp_export.log',resp.reason)
dologging('tp_export.log',json.dumps(data))
return 0, resp.reason, resp.status_code, headers
else:
return 1, "ok", 200, resp.headers
return 0, 0, 0, 0 # pragma: no cover
def workout_tp_upload(user, w): # pragma: no cover
message = "Uploading to TrainingPeaks"
tpid = 0
r = w.user
thetoken = tp_open(r.user)
# need some code if token doesn't refresh
if (is_workout_user(user, w)):
tcxfile = createtpworkoutdata(w)
if tcxfile:
res, reason, status_code, headers = uploadactivity(
thetoken, tcxfile,
name=w.name
)
if res == 0:
message = "Upload to TrainingPeaks failed with status code " + \
str(status_code)+": "+reason
w.tpid = -1
try:
os.remove(tcxfile)
except WindowsError:
pass
return message, tpid
else: # res != 0
w.uploadedtotp = res
tpid = res
w.save()
os.remove(tcxfile)
job = myqueue(queuelow,
check_tp_workout_id,
w,
headers['Location'])
return 'Successfully synchronized to TrainingPeaks', tpid
else: # no tcxfile
dologging('tp_export.log','Failed to create tcx file')
message = "Upload to TrainingPeaks failed"
w.uploadedtotp = -1
tpid = -1
w.save()
return message, tpid
else: # not allowed to upload
message = "You are not allowed to export this workout to TP"
tpid = 0
return message, tpid
return message, tpid

View File

@@ -1,7 +1,6 @@
from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, workouttypes_ordered from rowers.mytypes import workouttypes, boattypes, otwtypes, workoutsources, workouttypes_ordered
from rowers.rower_rules import is_promember from rowers.rower_rules import is_promember
import rowers.tpstuff as tpstuff
from rowers.integrations import * from rowers.integrations import *
from rowers.utils import ( from rowers.utils import (
@@ -270,9 +269,8 @@ def do_sync(w, options, quick=False):
upload_to_st = False upload_to_st = False
if do_tp_export: if do_tp_export:
try: try:
_, id = tpstuff.workout_tp_upload( tp_integration = TPIntegration(w.user.user)
w.user.user, w id = tp_integration.workout_export(w)
)
dologging('tp_export.log', dologging('tp_export.log',
'exported workout {wid} for user {uid}'.format( 'exported workout {wid} for user {uid}'.format(
wid = w.id, wid = w.id,

View File

@@ -27,8 +27,11 @@ def workout_tp_upload_view(request, id=0):
message = "" message = ""
r = getrower(request.user) r = getrower(request.user)
res = -1 res = -1
tp_integration = TPIntegration(request.user)
try: try:
_ = tp_open(r.user) _ = tp_integration.open()
except NoTokenError: # pragma: no cover except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/tpauthorize/") return HttpResponseRedirect("/rowers/me/tpauthorize/")
@@ -36,38 +39,9 @@ def workout_tp_upload_view(request, id=0):
w = get_workout_by_opaqueid(request, id) w = get_workout_by_opaqueid(request, id)
r = w.user r = w.user
tcxfile = tpstuff.createtpworkoutdata(w) jobid = tp_integration.workout_export(w)
if tcxfile: messages.info(request,'Your workout will be exported to TrainingPeaks in the background')
res, reason, status_code, headers = tpstuff.uploadactivity(
r.tptoken, tcxfile,
name=w.name
)
if res == 0: # pragma: no cover
message = "Upload to TrainingPeaks failed with status code " + \
str(status_code)+": "+reason
try:
os.remove(tcxfile)
except WindowsError:
pass
messages.error(request, message)
else: # res != 0
w.uploadedtotp = res
w.save()
os.remove(tcxfile)
job = myqueue(queuelow,
check_tp_workout_id,
w,
headers['Location'])
messages.info(request, 'Uploaded to TrainingPeaks')
else: # pragma: no cover # no tcxfile
message = "Upload to TrainingPeaks failed"
w.uploadedtotp = -1
w.save()
messages.error(request, message)
url = reverse(r.defaultlandingpage, url = reverse(r.defaultlandingpage,
kwargs={ kwargs={
@@ -301,20 +275,8 @@ def rower_c2_token_refresh(request):
@login_required() @login_required()
def rower_tp_token_refresh(request): def rower_tp_token_refresh(request):
r = getrower(request.user) r = getrower(request.user)
res = tpstuff.do_refresh_token( tp_integration = TPIntegration(request.user)
r.tprefreshtoken, token = tp_integration.token_refresh()
)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = getrower(request.user)
r.tptoken = access_token
r.tptokenexpirydate = expirydatetime
r.tprefreshtoken = refresh_token
r.save()
successmessage = "Tokens refreshed. Good to go" successmessage = "Tokens refreshed. Good to go"
messages.info(request, successmessage) messages.info(request, successmessage)
@@ -759,11 +721,8 @@ def rower_process_tpcallback(request):
url = reverse('rower_exportsettings_view') url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
res = tpstuff.get_token(code) tp_integration = TPIntegration(request.user)
access_token, expires_in, refresh_token = tp_integration.get_token(code)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = getrower(request.user) r = getrower(request.user)
@@ -1560,7 +1519,7 @@ importsources = {
'polar': polarstuff, 'polar': polarstuff,
'ownapi': ownapistuff, 'ownapi': ownapistuff,
'sporttracks': SportTracksIntegration, 'sporttracks': SportTracksIntegration,
'trainingpeaks': tpstuff, 'trainingpeaks': TPIntegration,
'nk': NKIntegration, 'nk': NKIntegration,
'rp3':RP3Integration, 'rp3':RP3Integration,
} }

View File

@@ -194,7 +194,6 @@ import datetime
import iso8601 import iso8601
import rowers.rojabo_stuff as rojabo_stuff import rowers.rojabo_stuff as rojabo_stuff
from rowers.tpstuff import tp_open
from iso8601 import ParseError from iso8601 import ParseError
import rowers.rojabo_stuff as rojabo_stuff import rowers.rojabo_stuff as rojabo_stuff
@@ -205,7 +204,6 @@ import rowers.polarstuff as polarstuff
from rowers.integrations import * from rowers.integrations import *
import rowers.tpstuff as tpstuff
import rowers.ownapistuff as ownapistuff import rowers.ownapistuff as ownapistuff
from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI

View File

@@ -5636,9 +5636,6 @@ def workout_upload_view(request,
except NoTokenError: except NoTokenError:
id = 0 id = 0
message = "Something went wrong with the Concept2 sync" message = "Something went wrong with the Concept2 sync"
if id > 1:
messages.info(request, message)
else:
messages.error(request, message) messages.error(request, message)
if (upload_to_strava): # pragma: no cover if (upload_to_strava): # pragma: no cover
@@ -5648,9 +5645,6 @@ def workout_upload_view(request,
except NoTokenError: except NoTokenError:
id = 0 id = 0
message = "Please connect to Strava first" message = "Please connect to Strava first"
if id > 1:
messages.info(request, message)
else:
messages.error(request, message) messages.error(request, message)
if (upload_to_st): # pragma: no cover if (upload_to_st): # pragma: no cover
@@ -5660,23 +5654,14 @@ def workout_upload_view(request,
except NoTokenError: except NoTokenError:
message = "Please connect to SportTracks first" message = "Please connect to SportTracks first"
id = 0 id = 0
if id > 1:
messages.info(request, message)
else:
messages.error(request, message) messages.error(request, message)
if (upload_to_tp): # pragma: no cover if (upload_to_tp): # pragma: no cover
tp_integration = TPIntegration(request.user)
try: try:
message, id = tpstuff.workout_tp_upload( id = tp_integration.workout_export(w)
request.user, w
)
except NoTokenError: except NoTokenError:
message = "Please connect to TrainingPeaks first" message = "Please connect to TrainingPeaks first"
id = 0
if id > 1:
messages.info(request, message)
else:
messages.error(request, message) messages.error(request, message)
if int(registrationid) < 0: # pragma: no cover if int(registrationid) < 0: # pragma: no cover