From 3eaef9ae7072c1932fb4c8e67b0f44a783f6a1ac Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 15 Feb 2023 19:25:45 +0100 Subject: [PATCH] doing trainingpeaks, untested --- rowers/integrations/__init__.py | 2 + rowers/integrations/rp3.py | 4 - rowers/integrations/trainingpeaks.py | 134 +++++++++++++++ rowers/tasks.py | 76 ++++++++- rowers/templates/menu_workout.html | 4 - rowers/tests/test_imports.py | 6 +- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4001 bytes rowers/tpstuff.py | 232 -------------------------- rowers/uploads.py | 6 +- rowers/views/importviews.py | 65 ++------ rowers/views/statements.py | 2 - rowers/views/workoutviews.py | 19 +-- 12 files changed, 230 insertions(+), 320 deletions(-) create mode 100644 rowers/integrations/trainingpeaks.py delete mode 100644 rowers/tpstuff.py diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index fff8ac50..06b659fd 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -3,3 +3,5 @@ from .strava import StravaIntegration from .nk import NKIntegration from .sporttracks import SportTracksIntegration from .rp3 import RP3Integration +from .trainingpeaks import TPIntegration + diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py index 657176ef..fb1230c4 100644 --- a/rowers/integrations/rp3.py +++ b/rowers/integrations/rp3.py @@ -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) diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py new file mode 100644 index 00000000..d9cea185 --- /dev/null +++ b/rowers/integrations/trainingpeaks.py @@ -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) + diff --git a/rowers/tasks.py b/rowers/tasks.py index 5430cf20..6418787a 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -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] diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html index bba56d91..6d8ff847 100644 --- a/rowers/templates/menu_workout.html +++ b/rowers/templates/menu_workout.html @@ -234,10 +234,6 @@ Connect to RP3 - {% else %} - - RP3 - {% endif %}
  • diff --git a/rowers/tests/test_imports.py b/rowers/tests/test_imports.py index 0bac3505..8ff6ab46 100644 --- a/rowers/tests/test_imports.py +++ b/rowers/tests/test_imports.py @@ -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/' diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 3133a666a6014bd9b73a9408007b84f22df346ba..dcef8d75f9aaf94262982865db76e4a6f2dbaf4f 100644 GIT binary patch literal 4001 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 GfB^v0#}*L) diff --git a/rowers/tpstuff.py b/rowers/tpstuff.py deleted file mode 100644 index 25ef8616..00000000 --- a/rowers/tpstuff.py +++ /dev/null @@ -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 diff --git a/rowers/uploads.py b/rowers/uploads.py index f99501a4..606d5a9e 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -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, diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 06f51140..4193d681 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -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, } diff --git a/rowers/views/statements.py b/rowers/views/statements.py index f3f2def6..d0526ca8 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -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 diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 6e8d0a62..4a72c3e0 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -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