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 .sporttracks import SportTracksIntegration
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)
# 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 traceback
from time import strftime
import base64
from io import BytesIO
from scipy import optimize
from scipy.signal import savgol_filter
@@ -105,7 +107,8 @@ except KeyError: # pragma: no cover
NK_API_LOCATION = CFG["nk_api_location"]
TP_CLIENT_ID = CFG["tp_client_id"]
TP_CLIENT_SECRET = CFG["tp_client_secret"]
TP_API_LOCATION = CFG["tp_api_location"]
tpapilocation = TP_API_LOCATION
from requests_oauthlib import OAuth1, OAuth1Session
@@ -344,6 +347,46 @@ def handle_add_workouts_team(ws, t, debug=False, **kwargs):
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
def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs):
authorizationstring = str('Bearer ' + workout.user.tptoken)
@@ -361,6 +404,37 @@ def check_tp_workout_id(workout, location, attempts=5, debug=False, **kwargs):
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
def instroke_static(w, metric, debug=False, **kwargs):
f1 = w.csvfilename[6:-4]

View File

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

View File

@@ -1373,15 +1373,15 @@ class TPObjects(DjangoTestCase):
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):
response = self.c.get('/rowers/me/tprefresh/',follow=True)
self.assertEqual(response.status_code, 200)
@patch('rowers.tpstuff.requests.post', side_effect=mocked_requests)
@patch('rowers.tpstuff.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.trainingpeaks.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.trainingpeaks.requests.get', side_effect=mocked_requests)
def test_tp_upload(self, mock_get, mock_post):
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.rower_rules import is_promember
import rowers.tpstuff as tpstuff
from rowers.integrations import *
from rowers.utils import (
@@ -270,9 +269,8 @@ def do_sync(w, options, quick=False):
upload_to_st = False
if do_tp_export:
try:
_, id = tpstuff.workout_tp_upload(
w.user.user, w
)
tp_integration = TPIntegration(w.user.user)
id = tp_integration.workout_export(w)
dologging('tp_export.log',
'exported workout {wid} for user {uid}'.format(
wid = w.id,

View File

@@ -23,12 +23,15 @@ def default(o): # pragma: no cover
# Send workout to TP
@permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True)
def workout_tp_upload_view(request, id=0):
message = ""
r = getrower(request.user)
res = -1
tp_integration = TPIntegration(request.user)
try:
_ = tp_open(r.user)
_ = tp_integration.open()
except NoTokenError: # pragma: no cover
return HttpResponseRedirect("/rowers/me/tpauthorize/")
@@ -36,38 +39,9 @@ def workout_tp_upload_view(request, id=0):
w = get_workout_by_opaqueid(request, id)
r = w.user
tcxfile = tpstuff.createtpworkoutdata(w)
if tcxfile:
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
jobid = tp_integration.workout_export(w)
messages.info(request,'Your workout will be exported to TrainingPeaks in the background')
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,
kwargs={
@@ -301,20 +275,8 @@ def rower_c2_token_refresh(request):
@login_required()
def rower_tp_token_refresh(request):
r = getrower(request.user)
res = tpstuff.do_refresh_token(
r.tprefreshtoken,
)
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()
tp_integration = TPIntegration(request.user)
token = tp_integration.token_refresh()
successmessage = "Tokens refreshed. Good to go"
messages.info(request, successmessage)
@@ -759,11 +721,8 @@ def rower_process_tpcallback(request):
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
res = tpstuff.get_token(code)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
tp_integration = TPIntegration(request.user)
access_token, expires_in, refresh_token = tp_integration.get_token(code)
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = getrower(request.user)
@@ -1560,7 +1519,7 @@ importsources = {
'polar': polarstuff,
'ownapi': ownapistuff,
'sporttracks': SportTracksIntegration,
'trainingpeaks': tpstuff,
'trainingpeaks': TPIntegration,
'nk': NKIntegration,
'rp3':RP3Integration,
}

View File

@@ -194,7 +194,6 @@ import datetime
import iso8601
import rowers.rojabo_stuff as rojabo_stuff
from rowers.tpstuff import tp_open
from iso8601 import ParseError
import rowers.rojabo_stuff as rojabo_stuff
@@ -205,7 +204,6 @@ import rowers.polarstuff as polarstuff
from rowers.integrations import *
import rowers.tpstuff as tpstuff
import rowers.ownapistuff as ownapistuff
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:
id = 0
message = "Something went wrong with the Concept2 sync"
if id > 1:
messages.info(request, message)
else:
messages.error(request, message)
if (upload_to_strava): # pragma: no cover
@@ -5648,9 +5645,6 @@ def workout_upload_view(request,
except NoTokenError:
id = 0
message = "Please connect to Strava first"
if id > 1:
messages.info(request, message)
else:
messages.error(request, message)
if (upload_to_st): # pragma: no cover
@@ -5660,23 +5654,14 @@ def workout_upload_view(request,
except NoTokenError:
message = "Please connect to SportTracks first"
id = 0
if id > 1:
messages.info(request, message)
else:
messages.error(request, message)
if (upload_to_tp): # pragma: no cover
tp_integration = TPIntegration(request.user)
try:
message, id = tpstuff.workout_tp_upload(
request.user, w
)
id = tp_integration.workout_export(w)
except NoTokenError:
message = "Please connect to TrainingPeaks first"
id = 0
if id > 1:
messages.info(request, message)
else:
messages.error(request, message)
if int(registrationid) < 0: # pragma: no cover