Private
Public Access
1
0

rp3, untested

This commit is contained in:
Sander Roosendaal
2023-02-14 20:57:42 +01:00
parent 4a70d7a3d0
commit 3c0fbc431d
9 changed files with 308 additions and 335 deletions

View File

@@ -2,3 +2,4 @@ from .c2 import C2Integration
from .strava import StravaIntegration
from .nk import NKIntegration
from .sporttracks import SportTracksIntegration
from .rp3 import RP3Integration

252
rowers/integrations/rp3.py Normal file
View File

@@ -0,0 +1,252 @@
from .integrations import SyncIntegration, NoTokenError
from rowers.models import User, Rower, Workout, TombStone
from rowers.tasks import handle_rp3_async_workout
from rowsandall_app.settings import (
RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET,
UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET
)
from rowers.utils import myqueue, NoTokenError, dologging, uniqify
from django.utils import timezone
import requests
import pandas as pd
import arrow
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('high')
from datetime import timedelta
graphql_url = "https://rp3rowing-app.com/graphql"
class RP3Integration(SyncIntegration):
def __init__(self, *args, **kwargs):
super(RP3Integration, self).__init__(*args, **kwargs)
self.oauth_data = {
'client_id': RP3_CLIENT_ID,
'client_secret': RP3_CLIENT_SECRET,
'redirect_uri': RP3_REDIRECT_URI,
'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?",
'content_type': 'application/x-www-form-urlencoded',
# 'content_type': 'application/json',
'tokenname': 'rp3token',
'refreshtokenname': 'rp3refreshtoken',
'expirydatename': 'rp3tokenexpirydate',
'bearer_auth': False,
'base_url': "https://rp3rowing-app.com/oauth/token",
'scope': 'read,write',
}
def createworkoutdata(self, w, *args, **kwargs):
return None
def workout_export(self, workout, *args, **kwargs) -> str:
pass
def get_workouts(self, *args, **kwargs) -> int:
auth_token = self.open()
r = self.rower
workouts_json = self.get_workout_list_json()
workouts_list = pd.json_normalize(workouts_json['data']['workouts'])
try:
rp3ids = workouts_list['id'].values
workouts_list.set_index('id',inpace=True)
except (KeyError, IndexError):
return 0
knownrp3ids = uniqify([
w.uploadedtorp3 for w in Workout.objects.filter(user=rower)
])
dologging('rp3_import.log',rp3ids)
newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids]
dologging('rp3_import.log',newids)
for id in newids:
startdatetime = workouts_list.loc[id, 'executed_at_ios8601']
dologging('rp3_import.log', startdatetime)
_ = myqueue(
queuehigh,
handle_rp3_async_workout,
self.user.id,
auth_token,
id,
startdatetime,
20,
{'timezone':self.rower.defaulttimezone}
)
return 1
def get_workout(self, id, *args, **kwargs) -> int:
startdatetime = kwargs.get('startdatetime', None)
if not startdatetime:
startdatetime = str(timezone.now())
auth_token = self.open()
_ = myqueue(
queuehigh,
handle_rp3_async_workout,
self.user.id,
auth_token,
id,
startdatetime,
20,
timezone = self.rower.defaulttimezone
)
def get_workout_schema(self, *args, **kwargs) -> dict:
auth_token = self.open()
headers = {'Authorization': 'Bearer ' + auth_token}
get_schema = """{
__type(name:"Workout") {
name
fields {
name
description
type {
name
kind
ofType {
name
kind
}
}
}
}
}"""
response = requests.post(
url = graphql_url,
headers=headers,
json={'query':get_schema}
)
return response.json()
def get_workout_list_json(self, *args, **kwargs) -> dict:
auth_token = self.open()
r = self.rower
headers = {'Authorization': 'Bearer ' + auth_token}
get_workouts_list = """{
workouts{
id
executed_at_iso8601
}
}"""
response = requests.post(
url=graphql_url,
headers=headers,
json={'query': get_workouts_list}
)
if (response.status_code != 200): # pragma: no cover
raise NoTokenError("Need to authorize")
return response.json()
def get_workout_list(self, *args, **kwargs) -> list:
r = self.rower
workouts_json = self.get_workout_list_json(*args, **kwargs)
workouts_list = pd.json_normalize(workouts_json['data']['workouts'])
knownrp3ids = uniqify([
w.uploadedtorp3 for w in Workout.objects.filter(user=r)
])
workouts = []
for key, data in workouts_list.iterrows():
print(data)
try:
i = data['id']
except KeyError: # pragma: no cover
i = 0
if i in knownrp3ids: # pragma: no cover
nnn = ''
else:
nnn = 'NEW'
try:
s = arrow.get(data['executed_at_iso8601']).isoformat()
except KeyError: # pragma: no cover
s = ''
keys = ['id', 'distance', 'duration', 'starttime',
'rowtype', 'source', 'name', 'new']
values = [i, '', '', s, '', 'rp3', '', nnn]
res = dict(zip(keys, values))
workouts.append(res)
return workouts
def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover
return super(RP3Integration, self).make_authorization_url(*args, **krags)
def get_token(self, code, *args, **kwargs) -> (str, int, str):
post_data = {
"client_id": RP3_CLIENT_KEY,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": RP3_REDIRECT_URI,
"client_secret": RP3_CLIENT_SECRET,
}
response = requests.post(
"https://rp3rowing-app.com/oauth/token",
data=post_data, verify=False,
)
try:
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except KeyError:
thetoken = ""
expires_in = 0
refresh_token = ""
return thetoken, expires_in, refresh_token
def open(self, *args, **kwargs) -> str:
tokenexpirydate = self.user.rower.rp3tokenexpirydate
if tokenexpirydate is None:
raise NoTokenError("No Token")
if tokenexpirydate is not None and timezone.now()-timedelta(days=120)>tokenexpirydate:
self.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1)
self.rower.save()
raise NoTokenError("No Token")
return super(RP3Integration, self).open()
def token_refresh(self, *args, **kwargs) -> str:
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

@@ -1,267 +0,0 @@
from celery import Celery, app
from rowers.rower_rules import is_workout_user
import time
from django_rq import job
from rowers.tasks import handle_rp3_async_workout
from rowsandall_app.settings import (
C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET,
STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET,
RP3_CLIENT_ID, RP3_CLIENT_KEY, RP3_REDIRECT_URI, RP3_CLIENT_SECRET,
UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET
)
from rowers.utils import myqueue, NoTokenError
# All the functionality needed to connect to Runkeeper
from rowers.imports import *
# Python
import gzip
from datetime import timedelta
import base64
from io import BytesIO
from rowers.utils import dologging
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('high')
oauth_data = {
'client_id': RP3_CLIENT_ID,
'client_secret': RP3_CLIENT_SECRET,
'redirect_uri': RP3_REDIRECT_URI,
'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?",
'content_type': 'application/x-www-form-urlencoded',
# 'content_type': 'application/json',
'tokenname': 'rp3token',
'refreshtokenname': 'rp3refreshtoken',
'expirydatename': 'rp3tokenexpirydate',
'bearer_auth': False,
'base_url': "https://rp3rowing-app.com/oauth/token",
'scope': 'read,write',
}
graphql_url = "https://rp3rowing-app.com/graphql"
# Checks if user has UnderArmour token, renews them if they are expired
def rp3_open(user):
tokenexpirydate = user.rower.rp3tokenexpirydate
if tokenexpirydate is None:
raise NoTokenError("No Token")
if tokenexpirydate is not None and timezone.now()-timedelta(days=120)>tokenexpirydate:
user.rower.rp3tokenexpirydate = timezone.now()-timedelta(days=1)
user.rower.save()
raise NoTokenError("No Token")
return imports_open(user, oauth_data)
# Refresh ST token using refresh token
def do_refresh_token(refreshtoken): # pragma: no cover
return imports_do_refresh_token(refreshtoken, oauth_data)
# Exchange access code for long-lived access token
def get_token(code): # pragma: no cover
post_data = {
"client_id": RP3_CLIENT_KEY,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": RP3_REDIRECT_URI,
"client_secret": RP3_CLIENT_SECRET,
}
response = requests.post(
"https://rp3rowing-app.com/oauth/token",
data=post_data, verify=False,
)
try:
token_json = response.json()
thetoken = token_json['access_token']
expires_in = token_json['expires_in']
refresh_token = token_json['refresh_token']
except KeyError:
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 get_rp3_workout_list(user):
auth_token = rp3_open(user)
headers = {'Authorization': 'Bearer ' + auth_token}
get_workouts_list = """{
workouts{
id
executed_at
}
}"""
response = requests.post(
url=graphql_url,
headers=headers,
json={'query': get_workouts_list}
)
return response
def get_rp3_workouts(rower, do_async=True): # pragma: no cover
try:
auth_token = rp3_open(rower.user)
except NoTokenError:
return 0
res = get_rp3_workout_list(rower.user)
if (res.status_code != 200):
return 0
s = '{d}'.format(d=res.json())
dologging('rp3_import.log', s)
workouts_list = pd.json_normalize(res.json()['data']['workouts'])
try:
rp3ids = workouts_list['id'].values
workouts_list.set_index('id', inplace=True)
except (KeyError, IndexError):
return 0
knownrp3ids = uniqify([
w.uploadedtorp3 for w in Workout.objects.filter(user=rower)
])
dologging('rp3_import.log',rp3ids)
newids = [rp3id for rp3id in rp3ids if rp3id not in knownrp3ids]
dologging('rp3_import.log',newids)
for id in newids:
startdatetime = workouts_list.loc[id, 'executed_at']
dologging('rp3_import.log', startdatetime)
_ = myqueue(
queuehigh,
handle_rp3_async_workout,
rower.user.id,
auth_token,
id,
startdatetime,
20,
)
return 1
def download_rp3_file(url, auth_token, filename): # pragma: no cover
headers = {'Authorization': 'Bearer ' + auth_token}
res = requests.get(url, headers=headers)
if res.status_code == 200:
with open(filename, 'wb') as f:
f.write(res.content)
return res.status_code
def get_rp3_workout_token(workout_id, auth_token, waittime=3, max_attempts=20): # pragma: no cover
headers = {'Authorization': 'Bearer ' + auth_token}
get_download_link = """{
download(workout_id: """ + str(workout_id) + """, type:csv){
id
status
link
}
}"""
have_link = False
download_url = ''
counter = 0
while not have_link:
response = requests.post(
url=graphql_url,
headers=headers,
json={'query': get_download_link}
)
if response.status_code != 200:
have_link = True
workout_download_details = pd.json_normalize(
response.json()['data']['download'])
if workout_download_details.iat[0, 1] == 'ready':
download_url = workout_download_details.iat[0, 2]
have_link = True
counter += 1
if counter > max_attempts:
have_link = True
time.sleep(waittime)
return download_url
def get_rp3_workout_link(user, workout_id, waittime=3, max_attempts=20): # pragma: no cover
auth_token = rp3_open(user)
return get_rp3_workout_token(workout_id, auth_token, waittime=waittime, max_attempts=max_attempts)
def get_rp3_workout(user, workout_id, startdatetime=None): # pragma: no cover
url = get_rp3_workout_link(user, workout_id)
filename = 'media/RP3Import_'+str(workout_id)+'.csv'
auth_token = rp3_open(user)
if not startdatetime:
startdatetime = str(timezone.now())
status_code = download_rp3_file(url, auth_token, filename)
if status_code != 200:
return 0
userid = user.id
uploadoptions = {
'secret': UPLOAD_SERVICE_SECRET,
'user': userid,
'file': filename,
'workouttype': 'dynamic',
'boattype': '1x',
'rp3id': workout_id,
'startdatetime': startdatetime,
'timezone': str(user.rower.defaulttimezone)
}
session = requests.session()
newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'}
session.headers.update(newHeaders)
response = session.post(UPLOAD_SERVICE_URL, json=uploadoptions)
if response.status_code != 200:
return 0
return response.json()['id']

View File

@@ -3158,6 +3158,9 @@ def handle_update_wps(rid, types, ids, mode, debug=False, **kwargs):
@app.task
def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempts, debug=False, **kwargs):
timezone = kwargs.get('timezone', 'UTC')
headers = {'Authorization': 'Bearer ' + rp3token}
get_download_link = """{
@@ -3239,8 +3242,11 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt
'boattype': '1x',
'rp3id': int(rp3id),
'startdatetime': startdatetime,
'timezone': timezone,
}
print(uploadoptions)
session = requests.session()
newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'}
session.headers.update(newHeaders)

View File

@@ -971,8 +971,8 @@ class RP3Objects(DjangoTestCase):
csvfilename=filename
)
@patch('rowers.rp3stuff.requests.get', side_effect=mocked_requests)
@patch('rowers.rp3stuff.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.rp3.requests.get', side_effect=mocked_requests)
@patch('rowers.integrations.rp3.requests.post', side_effect=mocked_requests)
@patch('rowers.dataprep.getsmallrowdata_db', side_effect=mocked_getsmallrowdata_db)
def test_rp3_import(self, mock_get, mockpost,
mocked_getsmallrowdata_db):
@@ -1002,7 +1002,7 @@ class RP3Objects(DjangoTestCase):
res = tasks.handle_rp3_async_workout(userid,rp3token,rp3id,startdatetime,max_attempts)
self.assertEqual(res,1)
@patch('rowers.rp3stuff.requests.post', side_effect=mocked_requests)
@patch('rowers.integrations.rp3.requests.post', side_effect=mocked_requests)
def notest_rp3_callback(self, mock_post):
response = self.c.get('/rp3_callback?code=absdef23&scope=read',follow=True)
self.assertEqual(response.status_code, 200)

Binary file not shown.

View File

@@ -235,9 +235,9 @@ def do_sync(w, options, quick=False):
do_st_export = w.user.sporttracks_auto_export
if options['sporttracksid'] != 0 and options['sporttracksid'] != '':
w.uploadedtosporttracks = options['sporttracksid']
sporttracksid = options.get('sporttracksid','')
if sporttracksid != 0 and sporttracksid != '':
w.uploadedtosporttracks = sporttracksid
w.save()
do_st_export = False
try: # pragma: no cover

View File

@@ -725,11 +725,9 @@ def rower_process_rp3callback(request): # pragma: no cover
url = reverse('rower_exportsettings_view')
return HttpResponseRedirect(url)
res = rp3stuff.get_token(code)
rp3_integration = RP3Integration(request.user)
access_token, expires_in, refresh_token = rp3_integration.get_token(code)
access_token = res[0]
expires_in = res[1]
refresh_token = res[2]
expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in)
r = getrower(request.user)
@@ -803,56 +801,39 @@ def rower_process_testcallback(request): # pragma: no cover
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
def workout_rp3import_view(request, userid=0):
r = getrequestrower(request, userid=userid)
rp3_integration = RP3Integration(request.user)
try:
_ = rp3stuff.rp3_open(request.user)
_ = rp3_integration.open()
except NoTokenError: # pragma: no cover
url = reverse('rower_rp3_authorize')
return HttpResponseRedirect(url)
res = rp3stuff.get_rp3_workout_list(request.user)
workouts = rp3_integration.get_workout_list()
datedict = {}
for workout in workouts:
datedict[workout['id']] = workout['starttime']
if (res.status_code != 200): # pragma: no cover
if (res.status_code == 401):
r = getrower(request.user)
if (r.stravatoken == '') or (r.stravatoken is None):
s = "Token doesn't exist. Need to authorize"
return HttpResponseRedirect("/rowers/me/stravaauthorize/")
message = "Something went wrong in workout_rp3import_view"
messages.error(request, message)
url = reverse('workouts_view')
return HttpResponseRedirect(url)
workouts_list = pd.json_normalize(res.json()['data']['workouts'])
if request.method == "POST":
try: # pragma: no cover
tdict = dict(request.POST.lists())
ids = tdict['workoutid']
rp3ids = [int(id) for id in ids]
knownrp3ids = uniqify([
w.uploadedtorp3 for w in Workout.objects.filter(user=r)
])
workouts = []
for key, data in workouts_list.iterrows():
try:
i = data['id']
for rp3id in rp3ids:
rp3_integration.get_workout(rp3id,startdatetime=datedict[rp3id])
# done, redirect to workouts list
messages.info(
request,
'Your RP3 workouts will be imported in the background.'
' It may take a few minutes before they appear.')
url = reverse('workouts_view')
return HttpResponseRedirect(url)
except KeyError: # pragma: no cover
i = 0
if i in knownrp3ids: # pragma: no cover
nnn = ''
else:
nnn = 'NEW'
try:
s = data['executed_at']
except KeyError: # pragma: no cover
s = ''
keys = ['id', 'starttime', 'new']
values = [i, s, nnn]
res = dict(zip(keys, values))
workouts.append(res)
pass
breadcrumbs = [
{
'url': '/rowers/list-workouts/',
@@ -864,13 +845,18 @@ def workout_rp3import_view(request, userid=0):
},
]
return render(request, 'rp3_list_import.html',
checknew = request.GET.get('selectallnew', False)
return render(request, 'list_import.html',
{
'workouts': workouts,
'rower': r,
'active': 'nav-workouts',
'breadcrumbs': breadcrumbs,
'teams': get_my_teams(request.user)
'teams': get_my_teams(request.user),
'integration': 'RP3',
'checknew': checknew,
})
# The page where you select which Strava workout to import
@@ -1461,7 +1447,8 @@ def workout_getrp3workout_all(request): # pragma: no cover
r = getrequestrower(request)
result = rp3stuff.get_rp3_workouts(r, do_async=True)
rp3_integration = RP3Integration(request.user)
result = rp3_integration.get_workouts()
if result:
messages.info(
@@ -1582,21 +1569,12 @@ def workout_getrp3importview(request, externalid):
r = getrequestrower(request)
if r.user != request.user: # pragma: no cover
messages.error(
request, 'You can only access your own workouts on the NK Logbook, not those of your athletes')
request, 'You can only access your own workouts on the RP3 Logbook, not those of your athletes')
url = reverse('workout_rp3import_view', kwargs={
'userid': request.user.id})
return HttpResponseRedirect(url)
token = rp3stuff.rp3_open(r.user)
startdatetime = request.GET.get('startdatetime')
_ = myqueue(queuehigh,
handle_rp3_async_workout,
r.user.id,
token,
externalid,
startdatetime,
20,
)
rp3_integration = RP3Integration(request.user)
result = rp3_integration.get_workout(externalid)
messages.info(request, 'The workout will be imported in the background')
@@ -1684,8 +1662,11 @@ def workout_getimportview(request, externalid, source='c2', do_async=True):
@login_required()
def workout_getsporttracksworkout_all(request):
st_integration = SportTracksIntegration(request.user)
_ = st_integration.get_workouts()
messages.info(request,"Your SportTracks workouts will be imported in the background")
try:
_ = st_integration.get_workouts()
messages.info(request,"Your SportTracks workouts will be imported in the background")
except NoTokenError:
messages.error(request,"You have to connect to SportTracks first")
url = reverse('workouts_view')
return HttpResponseRedirect(url)

View File

@@ -193,7 +193,7 @@ import sys
import datetime
import iso8601
import rowers.rojabo_stuff as rojabo_stuff
from rowers.rp3stuff import rp3_open
from rowers.tpstuff import tp_open
from iso8601 import ParseError
@@ -206,7 +206,7 @@ import rowers.polarstuff as polarstuff
from rowers.integrations import *
import rowers.tpstuff as tpstuff
import rowers.rp3stuff as rp3stuff
import rowers.ownapistuff as ownapistuff
from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI
from rowsandall_app.settings import (