diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index 92aed939..0f933238 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -101,923 +101,3 @@ def getagegrouprecord(age, sex='male', weightcategory='hwt', return power - -oauth_data = { - 'client_id': C2_CLIENT_ID, - 'client_secret': C2_CLIENT_SECRET, - 'redirect_uri': C2_REDIRECT_URI, - 'autorization_uri': "https://log.concept2.com/oauth/authorize", - 'content_type': 'application/x-www-form-urlencoded', - 'tokenname': 'c2token', - 'refreshtokenname': 'c2refreshtoken', - 'expirydatename': 'tokenexpirydate', - 'bearer_auth': True, - 'base_url': "https://log.concept2.com/oauth/access_token", - 'scope': 'write', -} - - -# Checks if user has Concept2 tokens, resets tokens if they are -# expired. -def c2_open(user): - r = Rower.objects.get(user=user) - if (r.c2token == '') or (r.c2token is None): - raise NoTokenError("User has no token") - else: - if (timezone.now() > r.tokenexpirydate): - res = rower_c2_token_refresh(user) - if res is None: # pragma: no cover - raise NoTokenError("User has no token") - if res[0] is not None: - thetoken = res[0] - else: # pragma: no cover - raise NoTokenError("User has no token") - else: - thetoken = r.c2token - - return thetoken - - -def get_c2_workouts(rower, page=1, do_async=True): - try: - _ = c2_open(rower.user) - except NoTokenError: # pragma: no cover - return 0 - - res = get_c2_workout_list(rower.user, page=page) - - if (res.status_code != 200): # pragma: no cover - return 0 - else: - c2ids = [item['id'] for item in res.json()['data']] - alldata = {} - for item in res.json()['data']: - alldata[item['id']] = item - - knownc2ids = [ - w.uploadedtoc2 for w in Workout.objects.filter(user=rower) - ] - - tombstones = [ - t.uploadedtoc2 for t in TombStone.objects.filter(user=rower) - ] - - # get "blocked" c2ids - parkedids = [] - try: - with open('c2blocked.json', 'r') as c2blocked: - jsondata = json.load(c2blocked) - parkedids = jsondata['ids'] - except FileNotFoundError: # pragma: no cover - pass - - knownc2ids = uniqify(knownc2ids+tombstones+parkedids) - - newids = [c2id for c2id in c2ids if c2id not in knownc2ids] - if settings.TESTING: - newids = c2ids - - newparkedids = uniqify(newids+parkedids) - - with open('c2blocked.json', 'wt') as c2blocked: - data = {'ids': newparkedids} - json.dump(data, c2blocked) - - counter = 0 - for c2id in newids: - if do_async: # pragma: no cover - _ = myqueue(queuehigh, - handle_c2_async_workout, - alldata, - rower.user.id, - rower.c2token, - c2id, - counter, - rower.defaulttimezone) - - counter = counter+1 - else: - _ = create_async_workout(alldata, rower.user, c2id) - - return 1 - -# get workout metrics, then relay stroke data to an asynchronous task - - -def create_async_workout(alldata, user, c2id): - data = alldata[c2id] - splitdata = None - - c2id = data['id'] - workouttype = data['type'] - startdatetime = iso8601.parse_date(data['date']) - - try: - title = data['name'] - except KeyError: - title = "" - try: - t = data['comments'].split('\n', 1)[0] - title += t[:40] - except: - title = '' - - # Create CSV file name and save data to CSV file - csvfilename = 'media/Import_'+str(c2id)+'.csv.gz' - - r = Rower.objects.get(user=user) - - authorizationstring = str('Bearer ' + r.c2token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - # url2 = "https://log.concept2.com/api/users/me/results"+str(c2id) - url = "https://log.concept2.com/api/users/me/results/"+str(c2id)+"/strokes" - try: - s = requests.get(url, headers=headers) - except ConnectionError: # pragma: no cover - return 0 - - if s.status_code != 200: # pragma: no cover - return 0 - - strokedata = pd.DataFrame.from_dict(s.json()['data']) - - res = make_cumvalues(0.1*strokedata['t']) - cum_time = res[0] - lapidx = res[1] - - starttimeunix = arrow.get(startdatetime).timestamp() - starttimeunix = starttimeunix-cum_time.max() - - unixtime = cum_time+starttimeunix - # unixtime[0] = starttimeunix - seconds = 0.1*strokedata.loc[:, 't'] - - nr_rows = len(unixtime) - - try: # pragma: no cover - latcoord = strokedata.loc[:, 'lat'] - loncoord = strokedata.loc[:, 'lon'] - except: - latcoord = np.zeros(nr_rows) - loncoord = np.zeros(nr_rows) - - try: - strokelength = strokedata.loc[:, 'strokelength'] - except: - strokelength = np.zeros(nr_rows) - - dist2 = 0.1*strokedata.loc[:, 'd'] - - try: - spm = strokedata.loc[:, 'spm'] - except KeyError: # pragma: no cover - spm = 0*dist2 - - try: - hr = strokedata.loc[:, 'hr'] - except KeyError: # pragma: no cover - hr = 0*spm - - pace = strokedata.loc[:, 'p']/10. - pace = np.clip(pace, 0, 1e4) - pace = pace.replace(0, 300) - - velo = 500./pace - power = 2.8*velo**3 - if workouttype == 'bike': # pragma: no cover - velo = 1000./pace - - df = pd.DataFrame({'TimeStamp (sec)': unixtime, - ' Horizontal (meters)': dist2, - ' Cadence (stokes/min)': spm, - ' HRCur (bpm)': hr, - ' longitude': loncoord, - ' latitude': latcoord, - ' Stroke500mPace (sec/500m)': pace, - ' Power (watts)': power, - ' DragFactor': np.zeros(nr_rows), - ' DriveLength (meters)': np.zeros(nr_rows), - ' StrokeDistance (meters)': strokelength, - ' DriveTime (ms)': np.zeros(nr_rows), - ' StrokeRecoveryTime (ms)': np.zeros(nr_rows), - ' AverageDriveForce (lbs)': np.zeros(nr_rows), - ' PeakDriveForce (lbs)': np.zeros(nr_rows), - ' lapIdx': lapidx, - ' WorkoutState': 4, - ' ElapsedTime (sec)': seconds, - 'cum_dist': dist2 - }) - - df.sort_values(by='TimeStamp (sec)', ascending=True) - - res = df.to_csv(csvfilename, index_label='index', - compression='gzip') - - userid = r.user.id - - uploadoptions = { - 'secret': UPLOAD_SERVICE_SECRET, - 'user': userid, - 'file': csvfilename, - 'title': title, - 'workouttype': workouttype, - 'boattype': '1x', - 'c2id': c2id, - } - - 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: # pragma: no cover - return 0 - - try: - workoutid = response.json()['id'] - except KeyError: # pragma: no cover - workoutid = 1 - - newc2id = Workout.objects.get(id=workoutid).uploadedtoc2 - - parkedids = [] - with open('c2blocked.json', 'r') as c2blocked: - jsondata = json.load(c2blocked) - parkedids = jsondata['ids'] - - newparkedids = [id for id in parkedids if id != newc2id] - with open('c2blocked.json', 'wt') as c2blocked: - data = {'ids': newparkedids} - c2blocked.seek(0) - json.dump(data, c2blocked) - - # summary - if 'workout' in data: # pragma: no cover - if 'splits' in data['workout']: - splitdata = data['workout']['splits'] - elif 'intervals' in data['workout']: - splitdata = data['workout']['intervals'] - else: - splitdata = False - else: - splitdata = False - - if splitdata: # pragma: no cover - summary, sa, results = c2stuff.summaryfromsplitdata( - splitdata, data, csvfilename, workouttype=workouttype) - w = Workout.objects.get(id=workoutid) - w.summary = summary - w.save() - - from rowingdata.trainingparser import getlist - if sa: - values = getlist(sa) - units = getlist(sa, sel='unit') - types = getlist(sa, sel='type') - - rowdata = rdata(w.csvfilename) - if rowdata: - rowdata.updateintervaldata(values, - units, types, results) - - rowdata.write_csv(w.csvfilename, gzip=True) - dataprep.update_strokedata(w.id, rowdata.df) - - return workoutid - - -# convert datetime object to seconds -def makeseconds(t): - seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5) - return seconds - -# convert our weight class code to Concept2 weight class code - - -def c2wc(weightclass): - if (weightclass == "lwt"): # pragma: no cover - res = "L" - else: - res = "H" - - return res - -# Concept2 logbook sends over split data for each interval -# We use it here to generate a custom summary -# Some users complained about small differences - - -def summaryfromsplitdata(splitdata, data, filename, sep='|', workouttype='rower'): - workouttype = workouttype.lower() - - totaldist = data['distance'] - totaltime = data['time']/10. - try: - spm = data['stroke_rate'] - except KeyError: # pragma: no cover - spm = 0 - try: - resttime = data['rest_time']/10. - except KeyError: # pragma: no cover - resttime = 0 - try: - restdistance = data['rest_distance'] - except KeyError: # pragma: no cover - restdistance = 0 - try: - avghr = data['heart_rate']['average'] - except KeyError: # pragma: no cover - avghr = 0 - try: - maxhr = data['heart_rate']['max'] - except KeyError: # pragma: no cover - maxhr = 0 - - try: - avgpace = 500.*totaltime/totaldist - except (ZeroDivisionError, OverflowError): # pragma: no cover - avgpace = 0. - - try: - restpace = 500.*resttime/restdistance - except (ZeroDivisionError, OverflowError): # pragma: no cover - restpace = 0. - - velo = totaldist/totaltime - avgpower = 2.8*velo**(3.0) - if workouttype in ['bike', 'bikeerg']: # pragma: no cover - velo = velo/2. - avgpower = 2.8*velo**(3.0) - velo = velo*2 - - try: - restvelo = restdistance/resttime - except (ZeroDivisionError, OverflowError): # pragma: no cover - restvelo = 0 - - restpower = 2.8*restvelo**(3.0) - if workouttype in ['bike', 'bikeerg']: # pragma: no cover - restvelo = restvelo/2. - restpower = 2.8*restvelo**(3.0) - restvelo = restvelo*2 - - try: - avgdps = totaldist/data['stroke_count'] - except (ZeroDivisionError, OverflowError, KeyError): # pragma: no cover - avgdps = 0 - - from rowingdata import summarystring, workstring, interval_string - - sums = summarystring(totaldist, totaltime, avgpace, spm, avghr, maxhr, - avgdps, avgpower, readFile=filename, - separator=sep) - - sums += workstring(totaldist, totaltime, avgpace, spm, avghr, maxhr, - avgdps, avgpower, separator=sep, symbol='W') - - sums += workstring(restdistance, resttime, restpace, 0, 0, 0, 0, restpower, - separator=sep, - symbol='R') - - sums += '\nWorkout Details\n' - sums += '#-{sep}SDist{sep}-Split-{sep}-SPace-{sep}-Pwr-{sep}SPM-{sep}AvgHR{sep}MaxHR{sep}DPS-\n'.format( - sep=sep - ) - - intervalnr = 0 - sa = [] - results = [] - - try: - timebased = data['workout_type'] in [ - 'FixedTimeSplits', 'FixedTimeInterval'] - except KeyError: # pragma: no cover - timebased = False - - for interval in splitdata: - try: - idist = interval['distance'] - except KeyError: # pragma: no cover - idist = 0 - - try: - itime = interval['time']/10. - except KeyError: # pragma: no cover - itime = 0 - try: - ipace = 500.*itime/idist - except (ZeroDivisionError, OverflowError): # pragma: no cover - ipace = 180. - - try: - ispm = interval['stroke_rate'] - except KeyError: # pragma: no cover - ispm = 0 - try: - irest_time = interval['rest_time']/10. - except KeyError: # pragma: no cover - irest_time = 0 - try: - iavghr = interval['heart_rate']['average'] - except KeyError: # pragma: no cover - iavghr = 0 - try: - imaxhr = interval['heart_rate']['average'] - except KeyError: # pragma: no cover - imaxhr = 0 - - # create interval values - iarr = [idist, 'meters', 'work'] - resarr = [itime] - if timebased: # pragma: no cover - iarr = [itime, 'seconds', 'work'] - resarr = [idist] - - if irest_time > 0: - iarr += [irest_time, 'seconds', 'rest'] - try: - resarr += [interval['rest_distance']] - except KeyError: # pragma: no cover - resarr += [np.nan] - - sa += iarr - results += resarr - - if itime != 0: - ivelo = idist/itime - ipower = 2.8*ivelo**(3.0) - if workouttype in ['bike', 'bikeerg']: # pragma: no cover - ipower = 2.8*(ivelo/2.)**(3.0) - else: # pragma: no cover - ivelo = 0 - ipower = 0 - - sums += interval_string(intervalnr, idist, itime, ipace, ispm, - iavghr, imaxhr, 0, ipower, separator=sep) - intervalnr += 1 - - return sums, sa, results - - -# Create the Data object for the stroke data to be sent to Concept2 logbook -# API -def createc2workoutdata(w): - filename = w.csvfilename - try: - row = rowingdata(csvfile=filename) - except IOError: # pragma: no cover - return 0 - - try: - averagehr = int(row.df[' HRCur (bpm)'].mean()) - maxhr = int(row.df[' HRCur (bpm)'].max()) - except (ValueError, KeyError): # pragma: no cover - averagehr = 0 - maxhr = 0 - - # Calculate intervalstats - itime, idist, itype = row.intervalstats_values() - try: - lapnames = row.df[' lapIdx'].unique() - except KeyError: # pragma: no cover - lapnames = range(len(itime)) - nrintervals = len(itime) - if len(lapnames) != nrintervals: - newlapnames = [] - for name in lapnames: - newlapnames += [name, name] - lapnames = newlapnames - intervaldata = [] - for i in range(nrintervals): - if itime[i] > 0: - mask = (row.df[' lapIdx'] == lapnames[i]) & ( - row.df[' WorkoutState'] == itype[i]) - try: - spmav = int(row.df[' Cadence (stokes/min)'] - [mask].mean().astype(int)) - hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int)) - except AttributeError: # pragma: no cover - try: - spmav = int(row.df[' Cadence (stokes/min)'][mask].mean()) - hrav = int(row.df[' HRCur (bpm)'][mask].mean()) - except ValueError: - spmav = 0 - try: - hrav = int(row.df[' HRCur (bpm)'][mask].mean()) - except ValuError: - hrav = 0 - intervaldict = { - 'type': 'distance', - 'time': int(10*itime[i]), - 'distance': int(idist[i]), - 'heart_rate': { - 'average': hrav, - }, - 'stroke_rate': spmav, - } - intervaldata.append(intervaldict) - - # adding diff, trying to see if this is valid - t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \ - 10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0] - try: - t[0] = t[1] - except IndexError: # pragma: no cover - pass - - d = 10*row.df.loc[:, ' Horizontal (meters)'].values - try: - d[0] = d[1] - except IndexError: # pragma: no cover - pass - - p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values) - p = np.clip(p, 0, 3600) - if w.workouttype == 'bike': # pragma: no cover - p = 2.0*p - - t = t.astype(int) - d = d.astype(int) - p = p.astype(int) - spm = row.df[' Cadence (stokes/min)'].astype(int) - - try: - spm[0] = spm[1] - except (KeyError, IndexError): # pragma: no cover - spm = 0*t - try: - hr = row.df[' HRCur (bpm)'].astype(int) - except ValueError: # pragma: no cover - hr = 0*d - stroke_data = [] - - t = t.tolist() - d = d.tolist() - p = p.tolist() - spm = spm.tolist() - hr = hr.tolist() - - for i in range(len(t)): - thisrecord = {"t": t[i], - "d": d[i], - "p": p[i], - "spm": spm[i], - "hr": hr[i]} - stroke_data.append(thisrecord) - - try: - durationstr = datetime.datetime.strptime( - str(w.duration), "%H:%M:%S.%f") - except ValueError: - durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S") - - workouttype = w.workouttype - if workouttype in otwtypes: - workouttype = 'water' - - if w.timezone == 'tzutc()': # pragma: no cover - w.timezone = 'UTC' - w.save() - - try: - wendtime = w.startdatetime.astimezone(pytz.timezone( - w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr)) - except UnknownTimeZoneError: - wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr)) - - data = { - "type": mytypes.c2mapping[workouttype], - # w.startdatetime.isoformat(), - "date": wendtime.strftime('%Y-%m-%d %H:%M:%S'), - "stroke_count": int(row.stroke_count), - "timezone": w.timezone, - "distance": int(w.distance), - "time": int(10*makeseconds(durationstr)), - "weight_class": c2wc(w.weightcategory), - "comments": w.notes, - 'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()), - 'drag_factor': int(row.dragfactor), - "heart_rate": { - "average": averagehr, - "max": maxhr, - }, - "stroke_data": stroke_data, - 'workout': { - 'splits': intervaldata, - } - } - - return data - -# Refresh Concept2 authorization token - - -def do_refresh_token(refreshtoken): - scope = "results:write,user:read" - # client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) - post_data = {"grant_type": "refresh_token", - "client_secret": C2_CLIENT_SECRET, - "client_id": C2_CLIENT_ID, - "refresh_token": refreshtoken, - } - headers = {'user-agent': 'sanderroosendaal'} - url = "https://log.concept2.com/oauth/access_token" - s = Session() - req = Request('POST', url, data=post_data, headers=headers) - - prepped = req.prepare() - prepped.body += "&scope=" - prepped.body += scope - - response = s.send(prepped) - - try: - token_json = response.json() - except JSONDecodeError: # pragma: no cover - return [None, None, None] - - try: - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - refresh_token = token_json['refresh_token'] - except: # pragma: no cover - with open("media/c2errors.log", "a") as errorlog: - errorstring = str(sys.exc_info()[0]) - timestr = time.strftime("%Y%m%d-%H%M%S") - errorlog.write(timestr+errorstring+"\r\n") - errorlog.write(str(token_json)+"\r\n") - thetoken = None - expires_in = None - refresh_token = None - - return [thetoken, expires_in, refresh_token] - -# Exchange authorization code for authorization token - - -def get_token(code): - messg = '' - scope = "user:read,results:write" - # client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) - post_data = {"grant_type": "authorization_code", - "code": code, - "redirect_uri": C2_REDIRECT_URI, - "client_secret": C2_CLIENT_SECRET, - "client_id": C2_CLIENT_ID, - } - headers = {'user-agent': 'sanderroosendaal'} - url = "https://log.concept2.com/oauth/access_token" - s = Session() - req = Request('POST', url, data=post_data, headers=headers) - - prepped = req.prepare() - prepped.body += "&scope=" - prepped.body += scope - - response = s.send(prepped) - - token_json = response.json() - - try: - status_code = response.status_code - # status_code = token_json['status_code'] - except AttributeError: # pragma: no cover - # except KeyError: - return (0, response.text) - try: - status_code = token_json.status_code - except AttributeError: # pragma: no cover - return (0, 'Attribute Error on c2_get_token') - - if status_code == 200: - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - refresh_token = token_json['refresh_token'] - else: # pragma: no cover - return (0, token_json['message']) - - return (thetoken, expires_in, refresh_token, messg) - -# Make URL for authorization and load it - - -def make_authorization_url(request): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - from uuid import uuid4 - # state = str(uuid4()) - scope = "user:read,results:write" - - params = {"client_id": C2_CLIENT_ID, - "response_type": "code", - "redirect_uri": C2_REDIRECT_URI} - url = "https://log.concept2.com/oauth/authorize?" + \ - urllib.parse.urlencode(params) - url += "&scope="+scope - - return HttpResponseRedirect(url) - -# Get workout from C2 ID - - -def get_workout(user, c2id, do_async=True): - r = Rower.objects.get(user=user) - _ = c2_open(user) - - _ = myqueue(queuehigh, - handle_c2_getworkout, - user.id, - r.c2token, - c2id, - r.defaulttimezone) - - return 1 - - -# Get list of C2 workouts. We load only the first page, -# assuming that users don't want to import their old workouts -def get_c2_workout_list(user, page=1): - r = Rower.objects.get(user=user) - if (r.c2token == '') or (r.c2token is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.tokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - - return custom_exception_handler(401, s) - - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.c2token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://log.concept2.com/api/users/me/results" - url += "?page={page}".format(page=page) - - s = requests.get(url, headers=headers) - - return s - - -# Get username, having access token. -# Handy for checking if the API access is working -def get_username(access_token): # pragma: no cover - authorizationstring = str('Bearer ' + access_token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - import urllib - url = "https://log.concept2.com/api/users/me" - response = requests.get(url, headers=headers) - - me_json = response.json() - - try: - res = me_json['data']['username'] - _ = me_json['data']['id'] - except KeyError: - res = None - - return res - -# Get user id, having access token -# Handy for checking if the API access is working - - -def get_userid(access_token): - authorizationstring = str('Bearer ' + access_token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - import urllib - url = "https://log.concept2.com/api/users/me" - try: - response = requests.get(url, headers=headers) - except: # pragma: no cover - return 0 - - try: - me_json = response.json() - except: # pragma: no cover - return 0 - try: - res = me_json['data']['id'] - except KeyError: # pragma: no cover - return 0 - - return res - -# For debugging purposes - - -def process_callback(request): # pragma: no cover - # need error handling - - code = request.GET['code'] - - access_token = get_token(code) - - username, id = get_username(access_token) - - return HttpResponse("got a user name: %s" % username) - - -def default(o): # pragma: no cover - if isinstance(o, numpy.int64): - return int(o) - raise TypeError - -# Uploading workout - - -def workout_c2_upload(user, w, asynchron=False): - message = 'trying C2 upload' - - try: - if mytypes.c2mapping[w.workouttype] is None: # pragma: no cover - return "This workout type cannot be uploaded to Concept2", 0 - except KeyError: # pragma: no cover - return "This workout type cannot be uploaded to Concept2", 0 - - _ = c2_open(user) - - r = Rower.objects.get(user=user) - - # ready to upload. Hurray - - if (is_workout_user(user, w)): - - c2userid = get_userid(r.c2token) - if not c2userid: # pragma: no cover - raise NoTokenError("User has no token") - - dologging('debuglog.log', - 'Upload to C2 user {userid}'.format(userid=user.id)) - data = createc2workoutdata(w) - dologging('debuglog.log', json.dumps(data)) - - if data == 0: # pragma: no cover - return "Error: No data file. Contact info@rowsandall.com if the problem persists", 0 - - authorizationstring = str('Bearer ' + r.c2token) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - import urllib - url = "https://log.concept2.com/api/users/%s/results" % (c2userid) - if not asynchron: # pragma: no cover - response = requests.post( - url, headers=headers, data=json.dumps(data, default=default)) - - if (response.status_code == 409): # pragma: no cover - message = "Concept2 Duplicate error" - w.uploadedtoc2 = -1 - c2id = -1 - w.save() - elif (response.status_code == 201 or response.status_code == 200): - # s= json.loads(response.text) - s = response.json() - c2id = s['data']['id'] - w.uploadedtoc2 = c2id - w.save() - message = "Upload to Concept2 was successful" - else: # pragma: no cover - message = "Something went wrong in workout_c2_upload_view." \ - " Response code 200/201 but C2 sync failed: "+response.text - c2id = 0 - else: # pragma: no cover - - _ = myqueue(queue, - handle_c2_sync, - w.id, - url, - headers, - json.dumps(data, default=default)) - c2id = 0 - - return message, c2id - -# This is token refresh. Looks for tokens in our database, then refreshes - - -def rower_c2_token_refresh(user): - r = Rower.objects.get(user=user) - res = do_refresh_token(r.c2refreshtoken) - if res[0]: - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) - - r = Rower.objects.get(user=user) - r.c2token = access_token - r.tokenexpirydate = expirydatetime - r.c2refreshtoken = refresh_token - - r.save() - return r.c2token - else: # pragma: no cover - return None diff --git a/rowers/imports.py b/rowers/imports.py index 6e40e0b5..38cac687 100644 --- a/rowers/imports.py +++ b/rowers/imports.py @@ -66,7 +66,7 @@ def splitstdata(lijst): return [np.array(t), np.array(latlong)] - +# covered in integrations def imports_open(user, oauth_data): r = Rower.objects.get(user=user) token = getattr(r, oauth_data['tokenname']) @@ -101,7 +101,7 @@ def imports_open(user, oauth_data): return token - +# covered in integrations # Refresh token using refresh token def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''): # client_auth = requests.auth.HTTPBasicAuth( @@ -175,7 +175,7 @@ def imports_do_refresh_token(refreshtoken, oauth_data, access_token=''): # Exchange ST access code for long-lived ST access token - +# implemented in integrations def imports_get_token( code, oauth_data ): diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py new file mode 100644 index 00000000..f68aed3e --- /dev/null +++ b/rowers/integrations/__init__.py @@ -0,0 +1,19 @@ +from .c2 import C2Integration +from .strava import StravaIntegration +from .nk import NKIntegration +from .sporttracks import SportTracksIntegration +from .rp3 import RP3Integration +from .trainingpeaks import TPIntegration +from .polar import PolarIntegration + +importsources = { + 'c2': C2Integration, + 'strava': StravaIntegration, + 'sporttracks': SportTracksIntegration, + 'trainingpeaks': TPIntegration, + 'nk': NKIntegration, + 'tp':TPIntegration, + 'rp3':RP3Integration, + 'polar': PolarIntegration +} + diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py new file mode 100644 index 00000000..60a066e3 --- /dev/null +++ b/rowers/integrations/c2.py @@ -0,0 +1,453 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone +from rowingdata import rowingdata +import numpy as np +import datetime +import json +import urllib +from rowers.utils import dologging, uniqify, custom_exception_handler +from django.utils import timezone +import requests +from pytz.exceptions import UnknownTimeZoneError +import pytz + +from rowsandall_app.settings import ( + C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET +) + +from rowers.tasks import ( + handle_c2_import_stroke_data, handle_c2_sync, handle_c2_async_workout, + handle_c2_getworkout +) +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 + +from requests import Request, Session + +import rowers.mytypes as mytypes +from rowers.rower_rules import is_workout_user, ispromember + +def default(o): # pragma: no cover + if isinstance(o, numpy.int64): + return int(o) + raise TypeError + +# convert datetime object to seconds +def makeseconds(t): + seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5) + return seconds + +# convert our weight class code to Concept2 weight class code + + +def c2wc(weightclass): + if (weightclass == "lwt"): # pragma: no cover + res = "L" + else: + res = "H" + + return res + + +class C2Integration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(C2Integration, self).__init__(*args, **kwargs) + self.oauth_data = { + 'client_id': C2_CLIENT_ID, + 'client_secret': C2_CLIENT_SECRET, + 'redirect_uri': C2_REDIRECT_URI, + 'autorization_uri': "https://log.concept2.com/oauth/authorize", + 'content_type': 'application/x-www-form-urlencoded', + 'tokenname': 'c2token', + 'refreshtokenname': 'c2refreshtoken', + 'expirydatename': 'tokenexpirydate', + 'bearer_auth': True, + 'base_url': "https://log.concept2.com/oauth/access_token", + 'scope': 'write', + } + + def get_name(self): + return "Concept2 Logbook" + + def get_shortname(self): + return "c2" + + + + def get_token(self, code, *args, **kwargs): + messg = '' + scope = "user:read,results:write" + + post_data = {"grant_type": "authorization_code", + "code": code, + "redirect_uri": C2_REDIRECT_URI, + "client_secret": C2_CLIENT_SECRET, + "client_id": C2_CLIENT_ID, + } + headers = {'user-agent': 'sanderroosendaal'} + url = "https://log.concept2.com/oauth/access_token" + s = Session() + req = Request('POST', url, data=post_data, headers=headers) + + prepped = req.prepare() + prepped.body += "&scope=" + prepped.body += scope + + response = s.send(prepped) + + token_json = response.json() + + try: + status_code = response.status_code + except AttributeError: # pragma: no cover + return (0, response.text) + + if status_code == 200: + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + refresh_token = token_json['refresh_token'] + else: # pragma: no cover + raise NoTokenError("No Token") + + + return (thetoken, expires_in, refresh_token, messg) + + + def open(self, *args, **kwargs): + return super(C2Integration, self).open(*args, **kwargs) + + def token_refresh(self, *args, **kwargs): + return super(C2Integration, self).token_refresh(*args, **kwargs) + + def make_authorization_url(self, *args, **kwargs): + scope = "user:read,results:write" + + params = {"client_id": C2_CLIENT_ID, + "response_type": "code", + "redirect_uri": C2_REDIRECT_URI} + url = "https://log.concept2.com/oauth/authorize?" + \ + urllib.parse.urlencode(params) + url += "&scope="+scope + return url + + def get_userid(self, *args, **kwargs): + _ = self.open() + access_token = self.rower.c2token + authorizationstring = str('Bearer ' + access_token) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://log.concept2.com/api/users/me" + try: + response = requests.get(url, headers=headers) + except: # pragma: no cover + return 0 + + try: + me_json = response.json() + except: # pragma: no cover + return 0 + try: + res = me_json['data']['id'] + except KeyError: # pragma: no cover + return 0 + + return res + + def createworkoutdata(self, w, *args, **kwargs) -> dict: + filename = w.csvfilename + try: + row = rowingdata(csvfile=filename) + except IOError: # pragma: no cover + return 0 + + try: + averagehr = int(row.df[' HRCur (bpm)'].mean()) + maxhr = int(row.df[' HRCur (bpm)'].max()) + except (ValueError, KeyError): # pragma: no cover + averagehr = 0 + maxhr = 0 + + # Calculate intervalstats + itime, idist, itype = row.intervalstats_values() + lapnames = range(len(itime)) + nrintervals = len(itime) + + try: + lapnames = row.df[' lapIdx'].unique() + except KeyError: # pragma: no cover + pass + + if len(lapnames) != nrintervals: + newlapnames = [] + for name in lapnames: + newlapnames += [name, name] + lapnames = newlapnames + intervaldata = [] + for i in range(nrintervals): + if itime[i] > 0: + mask = (row.df[' lapIdx'] == lapnames[i]) & ( + row.df[' WorkoutState'] == itype[i]) + try: + spmav = int(row.df[' Cadence (stokes/min)'] + [mask].mean().astype(int)) + hrav = int(row.df[' HRCur (bpm)'][mask].mean().astype(int)) + except AttributeError: # pragma: no cover + try: + spmav = int(row.df[' Cadence (stokes/min)'][mask].mean()) + hrav = int(row.df[' HRCur (bpm)'][mask].mean()) + except ValueError: + spmav = 0 + try: + hrav = int(row.df[' HRCur (bpm)'][mask].mean()) + except ValuError: + hrav = 0 + intervaldict = { + 'type': 'distance', + 'time': int(10*itime[i]), + 'distance': int(idist[i]), + 'heart_rate': { + 'average': hrav, + }, + 'stroke_rate': spmav, + } + intervaldata.append(intervaldict) + + # adding diff, trying to see if this is valid + t = 10*row.df.loc[:, 'TimeStamp (sec)'].values - \ + 10*row.df.loc[:, 'TimeStamp (sec)'].iloc[0] + try: + t[0] = t[1] + except IndexError: # pragma: no cover + pass + + d = 10*row.df.loc[:, ' Horizontal (meters)'].values + try: + d[0] = d[1] + except IndexError: # pragma: no cover + pass + + p = abs(10*row.df.loc[:, ' Stroke500mPace (sec/500m)'].values) + p = np.clip(p, 0, 3600) + if w.workouttype == 'bike': # pragma: no cover + p = 2.0*p + + t = t.astype(int) + d = d.astype(int) + p = p.astype(int) + spm = row.df[' Cadence (stokes/min)'].astype(int) + + try: + spm[0] = spm[1] + except (KeyError, IndexError): # pragma: no cover + spm = 0*t + try: + hr = row.df[' HRCur (bpm)'].astype(int) + except ValueError: # pragma: no cover + hr = 0*d + stroke_data = [] + + t = t.tolist() + d = d.tolist() + p = p.tolist() + spm = spm.tolist() + hr = hr.tolist() + + for i in range(len(t)): + thisrecord = {"t": t[i], + "d": d[i], + "p": p[i], + "spm": spm[i], + "hr": hr[i]} + stroke_data.append(thisrecord) + + try: + durationstr = datetime.datetime.strptime( + str(w.duration), "%H:%M:%S.%f") + except ValueError: + durationstr = datetime.datetime.strptime(str(w.duration), "%H:%M:%S") + + workouttype = w.workouttype + if workouttype in mytypes.otwtypes: + workouttype = 'water' + + if w.timezone == 'tzutc()': # pragma: no cover + w.timezone = 'UTC' + w.save() + + try: + wendtime = w.startdatetime.astimezone(pytz.timezone( + w.timezone))+datetime.timedelta(seconds=makeseconds(durationstr)) + except UnknownTimeZoneError: + wendtime = w.startdatetime.astimezone(pytz.utc)+datetime.timedelta(seconds=makeseconds(durationstr)) + + data = { + "type": mytypes.c2mapping[workouttype], + # w.startdatetime.isoformat(), + "date": wendtime.strftime('%Y-%m-%d %H:%M:%S'), + "stroke_count": int(row.stroke_count), + "timezone": w.timezone, + "distance": int(w.distance), + "time": int(10*makeseconds(durationstr)), + "weight_class": c2wc(w.weightcategory), + "comments": w.notes, + 'stroke_rate': int(row.df[' Cadence (stokes/min)'].mean()), + 'drag_factor': int(row.dragfactor), + "heart_rate": { + "average": averagehr, + "max": maxhr, + }, + "stroke_data": stroke_data, + 'workout': { + 'splits': intervaldata, + } + } + + return data + + + def workout_export(self, workout, *args, **kwargs) -> str: + try: + if mytypes.c2mapping[workout.workouttype] is None: # pragma: no cover + return "0" + except KeyError: # pragma: no cover + return "0" + + _ = self.open() + r = self.rower + user = self.user + + if (is_workout_user(user, workout)): + c2userid = self.get_userid() + if not c2userid: # pragma: no cover + raise NoTokenError("User has no token") + + dologging('debuglog.log', + 'Upload to C2 user {userid}'.format(userid=user.id)) + data = self.createworkoutdata(workout) + dologging('c2_log.log', json.dumps(data)) + + if data == 0: # pragma: no cover + return 0 + + authorizationstring = str('Bearer ' + r.c2token) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + + url = "https://log.concept2.com/api/users/%s/results" % (c2userid) + + _ = myqueue(queue, + handle_c2_sync, + workout.id, + url, + headers, + json.dumps(data, default=default)) + c2id = 0 + + return c2id + + + + def get_workout(self, id): + _ = self.open() + _ = myqueue(queuehigh, + handle_c2_getworkout, + self.user.id, + self.rower.c2token, + id, + self.rower.defaulttimezone) + + return 1 + + + def get_workouts(self, *args, **kwargs): + page = kwargs.get('page',1) + + try: + _ = self.open() + except NoTokenError: + return 0 + + # this part to get_workout_list + workouts = self.get_workout_list(page=page) + + for workout in workouts: + c2id = workout['id'] + if workout['new'] == 'NEW': + self.get_workout(c2id) + + return 1 + + # should be unified to workout list + def get_workout_list(self, *args, **kwargs): + page = kwargs.get('page',1) + r = self.rower + if (r.c2token == '') or (r.c2token is None): # pragma: no cover + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401, s) + elif (timezone.now() > r.tokenexpirydate): # pragma: no cover + s = "Token expired. Needs to refresh." + + return custom_exception_handler(401, s) + + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + r.c2token) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://log.concept2.com/api/users/me/results" + url += "?page={page}".format(page=page) + + res = requests.get(url, headers=headers) + + if (res.status_code != 200): # pragma: no cover + return [] + + workouts = [] + c2ids = [item['id'] for item in res.json()['data']] + knownc2ids = uniqify([ + w.uploadedtoc2 for w in Workout.objects.filter(user=self.rower) + ]) + tombstones = [ + t.uploadedtoc2 for t in TombStone.objects.filter(user=self.rower) + ] + parkedids = [] + try: + with open('c2blocked.json', 'r') as c2blocked: + jsondata = json.load(c2blocked) + parkedids = jsondata['ids'] + except: # pragma: no cover + pass + + knownc2ids = uniqify(knownc2ids+tombstones+parkedids) + for item in res.json()['data']: + d = item['distance'] + i = item['id'] + ttot = item['time_formatted'] + s = item['date'] + r = item['type'] + s2 = item['source'] + c = item['comments'] + if i in knownc2ids: + nnn = '' + else: # pragma: no cover + nnn = 'NEW' + keys = ['id', 'distance', 'duration', 'starttime', + 'rowtype', 'source', 'name', 'new'] + values = [i, d, ttot, s, r, s2, c, nnn] + ress = dict(zip(keys, values)) + workouts.append(ress) + + return workouts + + + + + + diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py new file mode 100644 index 00000000..df11b674 --- /dev/null +++ b/rowers/integrations/integrations.py @@ -0,0 +1,263 @@ +from abc import ABCMeta, ABC, abstractmethod +from importlib import import_module +from rowers.models import Rower, User +from rowers.utils import NoTokenError + +import requests +from django.utils import timezone +from datetime import timedelta +import arrow +import urllib +from uuid import uuid4 + +import json + +class SyncIntegration(metaclass=ABCMeta): + oauth_data = { + 'tokenname':'token', + 'expirydatename':'exp', + 'refreshtokenname':'r', + 'redirect_uri': 'r', + 'client_secret': 's', + 'base_uri': 's' + } + + user = User() + rower = Rower() + + def __init__(self, *args, **kwargs): + user = args[0] + self.user = user + self.rower = user.rower + + @classmethod + def __subclasshook__(cls, subclass): + return (hasattr(subclass, 'get_token') and + callable(subclass.get_token) or + NotImplemented) + + @abstractmethod + def get_name(self): + raise NotImplementedError + + @abstractmethod + def get_shortname(self): + raise NotImplementedError + + @abstractmethod + def createworkoutdata(self, w, *args, **kwargs): + return None + + @abstractmethod + def workout_export(self, workout, *args, **kwargs) -> str: + pass + + @abstractmethod + def get_workouts(self, *args, **kwargs) -> int: + pass + + @abstractmethod + def get_workout(self, id) -> int: + return 0 + + # need to unify workout list + @abstractmethod + def get_workout_list(self, *args, **kwargs) -> list: + return [] + + @abstractmethod + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + + params = {"client_id": self.oauth_data['client_id'], + "response_type": "code", + "redirect_uri": self.oauth_data['redirect_uri'], + "scope": self.oauth_data['scope'], + "state": state} + + url = self.oauth_data['authorization_uri']+urllib.parse.urlencode(params) + + return url + + + @abstractmethod + def get_token(self, code, *args, **kwargs) -> (str, int, str): + redirect_uri = self.oauth_data['redirect_uri'] + client_secret = self.oauth_data['client_secret'] + client_id = self.oauth_data['client_id'] + base_uri = self.oauth_data['base_url'] + + post_data = {"grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + "client_secret": client_secret, + "client_id": client_id, + } + + try: + headers = self.oauth_data['headers'] + except KeyError: + headers = {'Accept': 'application/json', + 'Api-Key': client_id, + 'Content-Type': 'application/json', + 'user-agent': 'sanderroosendaal'} + + if 'grant_type' in self.oauth_data: + if self.oauth_data['grant_type']: + post_data['grant_type'] = self.oauth_data['grant_type'] + if 'strava' in self.oauth_data['autorization_uri']: + post_data['grant_type'] = "authorization_code" + + if 'json' in self.oauth_data['content_type']: + response = requests.post( + base_uri, + data=json.dumps(post_data), + headers=headers) + else: # pragma: no cover + response = requests.post( + base_uri, + data=post_data, + headers=headers, verify=False) + + if response.status_code == 200 or response.status_code == 201: + token_json = response.json() + try: + thetoken = token_json['access_token'] + except KeyError: # pragma: no cover + raise NoTokenError("Failed to obtain token") + try: + refresh_token = token_json['refresh_token'] + except KeyError: # pragma: no cover + refresh_token = '' + try: + expires_in = token_json['expires_in'] + except KeyError: # pragma: no cover + try: + expires_at = arrow.get(token_json['expires_at']).timestamp() + expires_in = expires_at - arrow.now().timestamp() + except KeyError: # pragma: no cover + expires_in = 0 + try: + expires_in = int(expires_in) + except (ValueError, TypeError): # pragma: no cover + expires_in = 0 + else: # pragma: no cover + raise NoTokenError("Failed to obtain token") + + return [thetoken, expires_in, refresh_token] + + + @abstractmethod + def open(self, *args, **kwargs) -> str: + token = getattr(self.rower, self.oauth_data['tokenname']) + + try: + tokenexpirydate = getattr(self.rower, self.oauth_data['expirydatename']) + except (TypeError, AttributeError, KeyError): # pragma: no cover + tokenexpirydate = None + + if (token == '') or (token is None): + raise NoTokenError("User has no token") + else: + tokenname = self.oauth_data['tokenname'] + refreshtokenname = self.oauth_data['refreshtokenname'] + expirydatename = self.oauth_data['expirydatename'] + if tokenexpirydate and timezone.now()+timedelta(seconds=60) > tokenexpirydate: + token = self.token_refresh() + elif tokenexpirydate is None and expirydatename is not None and 'strava' in expirydatename: # pragma: no cover + token = self.token_refresh() + + return token + + def do_refresh_token(self, *args, **kwargs) -> (str, int, str): + refreshtoken = getattr(self.rower, self.oauth_data['refreshtokenname']) + access_token = kwargs.get('access_token','') + post_data = {"grant_type": "refresh_token", + "client_secret": self.oauth_data['client_secret'], + "client_id": self.oauth_data['client_id'], + "refresh_token": refreshtoken, + } + headers = {'user-agent': 'sanderroosendaal', + 'Accept': 'application/json', + 'Content-Type': self.oauth_data['content_type']} + + # for Strava + if 'grant_type' in self.oauth_data: + if self.oauth_data['grant_type']: + post_data['grant_type'] = self.oauth_data['grant_type'] + + if self.oauth_data['bearer_auth']: + headers['authorization'] = 'Bearer %s' % access_token + + baseurl = self.oauth_data['base_url'] + + if 'json' in self.oauth_data['content_type']: + try: + response = requests.post(baseurl, + data=json.dumps(post_data), + headers=headers, verify=False) + except: # pragma: no cover + raise NoTokenError("Failed to get token") + else: + try: + response = requests.post(baseurl, + data=post_data, + headers=headers, verify=False, + ) + except: # pragma: no cover + raise NoTokenError("Failed to get token") + + if response.status_code == 200 or response.status_code == 201: + token_json = response.json() + else: # pragma: no cover + raise NoTokenError("User has no token") + + try: + thetoken = token_json['access_token'] + except KeyError: # pragma: no cover + raise NoTokenError("User has no token") + + try: + expires_in = token_json['expires_in'] + except KeyError: + try: + expires_at = arrow.get(token_json['expires_at']).timestamp() + expires_in = expires_at - arrow.now().timestamp() + except KeyError: # pragma: no cover + expires_in = 0 + try: + refresh_token = token_json['refresh_token'] + except KeyError: # pragma: no cover + refresh_token = refreshtoken + try: + expires_in = int(expires_in) + except (TypeError, ValueError): # pragma: no cover + expires_in = 0 + + return [thetoken, expires_in, refresh_token] + + + @abstractmethod + def token_refresh(self, *args, **kwargs) -> str: + refreshtoken = getattr(self.rower, self.oauth_data['refreshtokenname']) + + if not refreshtoken: + refreshtoken = getattr(self.rower, self.oauth_data['tokenname']) + + access_token, expires_in, refresh_token = self.do_refresh_token() + expirydatetime = timezone.now()+timedelta(seconds=expires_in) + + setattr(self.rower, self.oauth_data['tokenname'], access_token) + if self.oauth_data['expirydatename'] is not None: + setattr(self.rower, self.oauth_data['expirydatename'], expirydatetime) + if self.oauth_data['refreshtokenname'] is not None: + setattr(self.rower, self.oauth_data['refreshtokenname'], refresh_token) + + self.rower.save() + + return access_token + + diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py new file mode 100644 index 00000000..987b8972 --- /dev/null +++ b/rowers/integrations/nk.py @@ -0,0 +1,316 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +from rowers import mytypes +from rowers.nkimportutils import * +from rowers.tasks import handle_nk_async_workout +from rowsandall_app.settings import ( + NK_CLIENT_ID, NK_REDIRECT_URI, NK_CLIENT_SECRET, + SITE_URL, NK_API_LOCATION, NK_OAUTH_LOCATION, + UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET, +) + +import time +from time import strftime +import urllib +import requests +import arrow +from json.decoder import JSONDecodeError +from django.utils import timezone +from rowers.utils import dologging, uniqify, custom_exception_handler, myqueue + +from requests_oauthlib import OAuth2Session +from requests.auth import HTTPBasicAuth + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('low') + +class NKIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(NKIntegration, self).__init__(*args, **kwargs) + self.oauth_data = { + 'client_id': NK_CLIENT_ID, + 'client_secret': NK_CLIENT_SECRET, + 'redirect_uri': NK_REDIRECT_URI, + 'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", + 'content_type': 'application/json', + 'tokenname': 'nktoken', + 'refreshtokenname': 'nkrefreshtoken', + 'expirydatename': 'nktokenexpirydate', + 'bearer_auth': True, + 'base_url': NK_OAUTH_LOCATION+"/oauth/token", + 'scope': 'read', + } + + def get_name(self): + return "NK Logbook" + + def get_shortname(self): + return "nk" + + def createworkoutdata(self, w, *args, **kwargs): + return None + + def workout_export(self, workout, *args, **kwargs) -> str: + return "" # there is no export + + def get_workouts(self, *args, **kwargs) -> int: + before = kwargs.get('before',0) + after = kwargs.get('after',0) + try: + _ = self.open() + except NoTokenError: # pragma: no cover + dologging("nklog.log","NK Token error for user {id}".format(id=rower.user.id)) + return 0 + + workouts = self.get_workout_list(before=before, after=after) + + for workout in workouts: + nkid = workout['id'] + if workout['new'] == 'NEW': + dologging('nklog.log','Queueing {id}'.format(id=nkid)) + self.get_workout(nkid) + + return 1 + + + def get_workout(self, id, *args, **kwargs) -> int: + startdate = kwargs.get('startdate','') + enddate = kwargs.get('enddate','') + _ = self.open() + r = self.rower + + before = 0 + after = 0 + if startdate: # pragma: no cover + startdate = arrow.get(startdate) + after = str(int(startdate.timestamp())*1000) + if enddate: # pragma: no cover + enddate = arrow.get(enddate) + before = str(int(enddate.timestamp())*1000) + + jsondata = self.get_workout_list_json(before=before, after=after) + + alldata = {} + + for item in jsondata: + alldata[item['id']] = item + + res = myqueue( + queuehigh, + handle_nk_async_workout, + alldata, + r.user.id, + r.nktoken, + id, + 0, + r.defaulttimezone, + ) + + return 1 + + + def get_workout_list_json(self, *args, **kwargs) -> dict: + before = kwargs.get('before',0) + after = kwargs.get('after',0) + + # For debugging + #startdate = '2021-01-01' + #enddate = '2021-06-01' + #before = arrow.get(enddate) + #before = str(int(before.timestamp()*1000)) + + #after = arrow.get(startdate) + #after = str(int(after.timestamp()*1000)) + + r = self.rower + authorizationstring = str('Bearer ' + r.nktoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json', + } + + url = NK_API_LOCATION+"api/v1/sessions" + + params = { + 'after': after, + 'before': before, + } # start / end time + + res = requests.get(url, headers=headers, params=params) + if (res.status_code != 200): # pragma: no cover + raise NoTokenError("No NK Token") + + + return res.json() + + # need to unify workout list + def get_workout_list(self, *args, **kwargs) -> list: + _ = self.open() + r = self.rower + + before = kwargs.get('before',0) + after = kwargs.get('after',0) + + # ready to fetch. Hurray + if not before: # pragma: no cover + before = arrow.now()+timedelta(days=1) + before = str(int(before.timestamp())*1000) + if not after: # pragma: no cover + after = arrow.now()-timedelta(days=7) + after = str(int(after.timestamp())*1000) + + jsondata = self.get_workout_list_json(before=before,after=after) + + + # get NK IDs + nkids = [item['id'] for item in jsondata] + knownnkids = uniqify([ + w.uploadedtonk for w in Workout.objects.filter(user=r) + ]) + tombstones = [ + t.uploadedtonk for t in TombStone.objects.filter(user=r) + ] + parkedids = [] + try: + with open('nkblocked.json', 'r') as nkblocked: + try: + jsondatal = json.load(nkblocked) + except: + jsondatal = { + 'ids':[] + } + parkedids = jsondatal['ids'] + except FileNotFoundError: # pragma: no cover + pass + + knownnkids = uniqify(knownnkids+tombstones+parkedids) + workouts = [] + + + for item in jsondata: + d = int(float(item['totalDistanceGps'])) # could also be Impeller + i = item['id'] + n = item['name'] + if i in knownnkids: + nnn = '' + else: # pragma: no cover + nnn = 'NEW' + ttot = str(datetime.timedelta( + seconds=int(float(item['elapsedTime'])/1000.))) + s = arrow.get(item['startTime'], tzinfo=r.defaulttimezone).format( + arrow.FORMAT_RFC850) + keys = ['id', 'distance', 'duration', 'starttime', + 'rowtype', 'source', 'name','new'] + values = [i, d, ttot, s, None, None, n, nnn] + rs = dict(zip(keys, values)) + workouts.append(rs) + + workouts = workouts[::-1] + + return workouts + + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + state = str(uuid4()) + scope = "read" + params = { + "grant_type": "authorization_code", + "response_type": "code", + "client_id": NK_CLIENT_ID, + "scope": scope, + "state": state, + "redirect_uri": NK_REDIRECT_URI, + } + + url = NK_OAUTH_LOCATION+"/oauth/authorize?"+urllib.parse.urlencode(params) + return url + + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + url = self.oauth_data['base_url'] + + post_data = {"client_id": self.oauth_data['client_id'], + "grant_type": "authorization_code", + "redirect_uri": self.oauth_data['redirect_uri'], + "code": code, + } + + response = requests.post( + url, + auth=HTTPBasicAuth(self.oauth_data['client_id'], + self.oauth_data['client_secret']), + data=post_data + ) + + if response.status_code != 200: + raise NoTokenError("Failed to obtain token") + + token_json = response.json() + + access_token = token_json['access_token'] + refresh_token = token_json['refresh_token'] + expires_in = token_json['expires_in'] + nk_owner_id = token_json['user_id'] + + return [access_token, expires_in, refresh_token] #, nk_owner_id] + + def open(self, *args, **kwargs) -> str: + r = self.rower + if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover + raise NoTokenError("User has no token") + else: + if (timezone.now() > r.nktokenexpirydate): + thetoken = self.token_refresh() + if thetoken is None: # pragma: no cover + raise NoTokenError("User has no token") + return thetoken + else: + thetoken = r.nktoken + + return thetoken + + def do_refresh_token(self, *args, **kwargs): + post_data = {"grant_type": "refresh_token", + # "client_id":NK_CLIENT_ID, + "refresh_token": self.rower.nkrefreshtoken, + } + + url = self.oauth_data['base_url'] + + response = requests.post( + url, + data=post_data, + auth=HTTPBasicAuth( + self.oauth_data['client_id'], + self.oauth_data['client_secret'] + ) + ) + + if response.status_code != 200: # pragma: no cover + return [0, 0, 0] + + token_json = response.json() + + access_token = token_json['access_token'] + refresh_token = token_json['refresh_token'] + expires_in = token_json['expires_in'] + + return access_token, expires_in, refresh_token + + + def token_refresh(self, *args, **kwargs) -> str: + r = self.rower + access_token, expires_in, refresh_token = self.do_refresh_token() + + expirydatetime = timezone.now()+timedelta(seconds=expires_in) + + r.nktoken = access_token + r.nktokenexpirydate = expirydatetime + r.nkrefreshtoken = refresh_token + r.save() + + return r.nktoken + diff --git a/rowers/integrations/polar.py b/rowers/integrations/polar.py new file mode 100644 index 00000000..8b0b9768 --- /dev/null +++ b/rowers/integrations/polar.py @@ -0,0 +1,521 @@ +from rowers.rower_rules import ispromember +from .integrations import SyncIntegration +from rowers.models import User, Rower, Workout +from rowsandall_app.settings import ( + POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET, UPLOAD_SERVICE_URL +) + +import urllib +import requests + +from rowers.utils import dologging, myqueue, NoTokenError +from django.utils import timezone + +from uuid import uuid4 +import base64 +from rowers.tasks import handle_request_post +from json.decoder import JSONDecodeError + +from rowers.opaque import encoder +import rowers.mytypes as mytypes + +from django.conf import settings + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +baseurl = 'https://polaraccesslink.com/v3' + + +class PolarIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + if args[0] is not None: + super(PolarIntegration, self).__init__(*args, **kwargs) + + def get_notifications(self): + url = baseurl+'/notifications' + # state = str(uuid4()) + auth_string = '{id}:{secret}'.format( + id=POLAR_CLIENT_ID, + secret=POLAR_CLIENT_SECRET + ) + + try: + headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} + except TypeError: + headers = {'Authorization': 'Basic %s' % base64.b64encode( + bytes(auth_string, 'utf-8')).decode('utf-8')} + + try: + response = requests.get(url, headers=headers) + except ConnectionError: # pragma: no cover + response = { + 'status_code': 400, + } + + available_data = [] + + try: + if response.status_code == 200: + available_data = response.json()['available-user-data'] + dologging('polar.log', available_data) + else: # pragma: no cover + dologging('polar.log', response.status_code) + dologging('polar.log', response.text) + except AttributeError: # pragma: no cover + try: + dologging('polar.log', response.text) + except AttributeError: + pass + pass + + return available_data + + + def get_name(self): + return "Polar Flow" + + def get_shortname(self): + raise "polar" + + def createworkoutdata(self, w, *args, **kwargs): + raise NotImplementedError + + + def workout_export(self, workout, *args, **kwargs) -> str: + raise NotImplementedError + + def revoke_access(self): # pragma: no cover + user = self.user + headers = { + 'Authorization': 'Bearer {token}'.format(token=user.rower.polartoken) + } + + response = requests.delete('https://www.polaraccesslink.com/v3/users/{userid}'.format( + userid=user.rower.polaruserid + ), headers=headers) + + dologging('polar.log', response.text) + dologging('polar.log', response.reason) + + return 1 + + def get_polar_workouts(self, user): + r = Rower.objects.get(user=user) + + exercise_list = [] + + if (r.polartoken == '') or (r.polartoken is None): + s = "Token doesn't exist. Need to authorize" + return [] + elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover + s = "Token expired. Needs to refresh" + dologging('polar.log', s) + return [] + + authorizationstring = str('Bearer ' + r.polartoken) + headers = {'Authorization': authorizationstring, + 'Accept': 'application/json'} + + headers2 = { + 'Authorization': authorizationstring, + } + + url = baseurl+'/users/{userid}/exercise-transactions'.format( + userid=r.polaruserid + ) + + response = requests.post(url, headers=headers) + dologging('polar.log', url) + dologging('polar.log', authorizationstring) + dologging('polar.log', str(response.status_code)) + + if response.status_code == 201: + transactionid = response.json()['transaction-id'] + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + + dologging('polar.log', url) + + response = requests.get(url, headers=headers) + if response.status_code == 200: + exerciseurls = response.json()['exercises'] + dologging('polar.log', exerciseurls) + for exerciseurl in exerciseurls: + response = requests.get(exerciseurl, headers=headers) + if response.status_code == 200: + exercise_dict = response.json() + tcxuri = exerciseurl+'/tcx' + response = requests.get(tcxuri, headers=headers2) + + if response.status_code == 200: + filename = 'media/mailbox_attachments/{code}_{id}.tcx'.format( + id=exercise_dict['id'], + code=uuid4().hex[:16] + ) + dologging('polar.log', filename) + + with open(filename, 'wb') as fop: + fop.write(response.content) + + workouttype = 'other' + try: + workouttype = mytypes.polaraccesslink_sports[ + exercise_dict['detailed-sport-info']] + except KeyError: # pragma: no cover + dologging( + 'polar.log', exercise_dict['detailed-sport-info']) + dologging('polar.log', workouttype) + try: + workouttype = mytypes.polarmappinginv[exercise_dict['sport'].lower( + )] + except KeyError: + dologging('polar.log', workouttype) + pass + + dologging('polar.log', workouttype) + + # post file to upload api + # TODO: add workouttype + uploadoptions = { + 'title': '', + 'workouttype': workouttype, + 'boattype': '1x', + 'user': user.id, + 'secret': settings.UPLOAD_SERVICE_SECRET, + 'file': filename, + 'title': '', + } + + url = settings.UPLOAD_SERVICE_URL + + dologging('polar.log', uploadoptions) + dologging('polar.log', url) + + _ = myqueue( + queuehigh, + handle_request_post, + url, + uploadoptions + ) + + dologging('polar.log', response.status_code) + if response.status_code != 200: # pragma: no cover + try: + dologging('polar.log', response.text) + except: + pass + try: + dologging('polar.log', response.json()) + except: + pass + + exercise_dict['filename'] = filename + else: # pragma: no cover + exercise_dict['filename'] = '' + + exercise_list.append(exercise_dict) + dologging('polar.log', str(exercise_dict)) + + # commit transaction + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + requests.put(url, headers=headers) + dologging( + 'polar.log', 'Committed transation at {url}'.format(url=url)) + + return exercise_list + + + def get_workouts(self, *args, **kwargs) -> int: + available_data = self.get_notifications() + polaruserid = self.rower.polaruserid + for record in available_data: + dologging('polar.log', str(record)) + + if record['data-type'] == 'EXERCISE': + try: + r = Rower.objects.get(polaruserid=record['user-id']) + u = r.user + if r.polar_auto_import and ispromember(u): + exercise_list = self.get_polar_workouts(u) + dologging('polar.log', exercise_list) + elif record['user-id'] == polaruserid: + exercise_list = self.get_polar_workouts(u) + except Rower.DoesNotExist: # pragma: no cover + pass + + return 1 + + def register_user(self, token): + _ = self.open() + + authorizationstring = 'Bearer {token}'.format(token=token) + headers = { + 'Content-Type': 'application/xml', + 'Authorization': authorizationstring, + 'Accept': 'application/json' + } + + payload = { + "member-id": encoder.encode_hex(self.user.id) + } + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {token}'.format(token=token) + } + + dologging('polar.log', 'Registering user') + + response = requests.post( + 'https://www.polaraccesslink.com/v3/users', + json=payload, + headers=headers + ) + + + + if response.status_code not in [200, 201]: # pragma: no cover + # dologging('polar.log',url) + dologging('polar.log', headers) + dologging('polar.log', payload) + dologging('polar.log', response.status_code) + dologging('polar.log', response.content) + try: + dologging('polar.log', response.reason) + dologging('polar.log', response.text) + except KeyError: + pass + + try: + jsondata = response.json() + if jsondata['error']['error_type'] == 'user_already_registered': + return jsondata + except: + pass + + return {} + + polar_user_data = response.json() + + return polar_user_data + + def get_polar_user_info(self, physical=False): # pragma: no cover + r = self.rower + _ = self.open() + + authorizationstring = str('Bearer ' + r.polartoken) + headers = { + 'Authorization': authorizationstring, + 'Accept': 'application/json' + } + + if not physical: + url = baseurl+'/users/{userid}'.format( + userid=r.polaruserid + ) + else: + url = 'https://www.polaraccesslink.com/v3/users/{userid}/physical-information-transactions/'.format( + userid=r.polaruserid + ) + + if physical: + response = requests.post(url, headers=headers) + else: + response = requests.get(url, headers=headers) + + return response + + + def get_workout(self, id, transaction_id) -> int: + r = self.rower + _ = self.open() + authorizationstring = str('Bearer ' + r.polartoken) + headers = { + 'Authorization': authorizationstring, + 'Accept': 'application/json' + } + + url = baseurl+'/users/{userid}/exercise-transactions'.format( + userid=r.polaruserid + ) + + response = requests.post(url, headers=headers) + + if response.status_code == 201: + transactionid = response.json()['transaction-id'] + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + + response = requests.get(url, headers=headers) + if response.status_code == 200: + exerciseurls = response.json()['exercises'] + for exerciseurl in exerciseurls: + response = requests.get(exerciseurl, headers=headers) + if response.status_code == 200: + exercise_dict = response.json() + thisid = exercise_dict['id'] + if thisid == id: + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}' \ + '/exercises/{exerciseid}/tcx'.format( + userid=r.polaruserid, + transactionid=transactionid, + exerciseid=id) + authorizationstring = str('Bearer ' + r.polartoken) + headers2 = { + 'Authorization': authorizationstring, + } + + response = requests.get(url, headers=headers2) + + if response.status_code == 200: + result = response.content + # commit transaction + url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( + transactionid=transactionid, + userid=r.polaruserid + ) + response = requests.put(url, headers=headers) + dologging( + 'polar.log', 'Committing transaction on {url}'.format(url=url)) + else: # pragma: no cover + result = None + + return result + + + def get_workout_list(self, *args, **kwargs) -> list: + exercises = self.get_polar_workouts(self.user) + workouts = [] + try: + a = exercises.status_code + return [] + except: + return [] + + for exercise in exercises: + try: + d = exercise['distance'] + except KeyError: + d = 0 + + i = exercise['id'] + transactionid = exercise['transaction-id'] + starttime = exercise['start-time'] + rowtype = exercise['sport'] + durationstring = exercise['duration'] + duration = isodate.parse_duration(durationstring) + keys = ['id', 'distance', 'duration', + 'starttime', 'rowtype', 'source', 'name', 'new'] + values = [i, d, duration, starttime, rowtype, transactionid, '', ''] + res = dict(zip(keys, values)) + workouts.append(res) + + return workouts + + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + + state = str(uuid4()) + + params = {"client_id": POLAR_CLIENT_ID, + "response_type": "code", + # "redirect_uri": POLAR_REDIRECT_URI, + "state": state, + # "scope":"accesslink.read_all" + } + url = "https://flow.polar.com/oauth2/authorization?" + \ + urllib.parse.urlencode(params) + dologging('polar.log', 'Authorizing') + dologging('polar.log', url) + dologging('polar.log', params) + + return url + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + post_data = {"grant_type": "authorization_code", + "code": code, + # "redirect_uri": POLAR_REDIRECT_URI, + } + + auth_string = '{id}:{secret}'.format( + id=POLAR_CLIENT_ID, + secret=POLAR_CLIENT_SECRET + ) + + try: + headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} + except TypeError: + headers = {'Authorization': 'Basic %s' % base64.b64encode( + bytes(auth_string, 'utf-8')).decode('utf-8')} + + dologging('polar.log', 'Getting token') + dologging('polar.log', post_data) + dologging('polar.log', auth_string) + + response = requests.post("https://polarremote.com/v2/oauth2/token", + data=post_data, + headers=headers) + + if response.status_code != 200: # pragma: no cover + dologging('polar.log', 'Getting token, got:') + dologging('polar.log', response.status_code) + dologging('polar.log', response.reason) + dologging('polar.log', response.text) + + try: + token_json = response.json() + thetoken = token_json['access_token'] + expires_in = token_json['expires_in'] + user_id = token_json['x_user_id'] + dologging('polar.log', response.status_code) + try: + dologging('polar.log', response.text) + except AttributeError: + pass + dologging('polar.log', token_json) + except (KeyError, JSONDecodeError) as e: # pragma: no cover + dologging('polar.log', e) + try: + dologging('polar.log', response.text) + except AttributeError: + pass + thetoken = 0 + expires_in = 0 + user_id = 0 + + return [thetoken, expires_in, user_id] + + + def open(self, *args, **kwargs) -> str: + r = self.rower + if (r.polartoken == '') or (r.polartoken is None): + s = "Token doesn't exist. Need to authorize" + raise NoTokenError(s) + elif (timezone.now() > r.polartokenexpirydate): + s = "Token expired. Needs to refresh" + raise NoTokenError(s) + + token = self.rower.polartoken + + return token + + + def token_refresh(self, *args, **kwargs) -> str: + raise NotImplementedError + +# just as a quick test during development +u = User.objects.get(id=1) + +nk_integration_1 = PolarIntegration(u) + diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py new file mode 100644 index 00000000..02c8f8d0 --- /dev/null +++ b/rowers/integrations/rp3.py @@ -0,0 +1,259 @@ +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 get_name(self): + return "RP3 Logbook" + + def get_shortname(self): + return "rp3" + + 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',inplace=True) + except (KeyError, IndexError): + return 0 + + knownrp3ids = uniqify([ + w.uploadedtorp3 for w in Workout.objects.filter(user=r) + ]) + + 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_iso8601'] + 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(): + 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 + params = {"client_id": RP3_CLIENT_KEY, + "response_type": "code", + "redirect_uri": RP3_REDIRECT_URI, + } + url = "https://rp3rowing-app.com/oauth/authorize/?" + \ + urllib.parse.urlencode(params) + + return url + + 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) + + + diff --git a/rowers/integrations/sporttracks.py b/rowers/integrations/sporttracks.py new file mode 100644 index 00000000..199f95a9 --- /dev/null +++ b/rowers/integrations/sporttracks.py @@ -0,0 +1,325 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +from rowingdata import rowingdata + +from rowers.tasks import handle_sporttracks_sync, handle_sporttracks_workout_from_data +from rowers.rower_rules import is_workout_user +import rowers.mytypes as mytypes +from rowsandall_app.settings import ( + SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, + SPORTTRACKS_REDIRECT_URI +) + +import re +import numpy +import pytz +import json +import requests +import datetime +import pandas as pd + +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, uniqify + +def getidfromuri(uri): # pragma: no cover + m = re.search('/(\w.*)\/(\d+)', uri) + return m.group(2) + +def getidfromresponse(response): # pragma: no cover + t = response.json() + uri = t['uris'][0] + regex = '.*?sporttracks\.mobi\/api\/v2\/fitnessActivities/(\d+)\.json$' + m = re.compile(regex).match(uri).group(1) + + id = int(m) + + return int(id) + + +def default(o): # pragma: no cover + if isinstance(o, numpy.int64): + return int(o) + raise TypeError + +class SportTracksIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(SportTracksIntegration, self).__init__(*args, **kwargs) + + self.oauth_data = { + 'client_id': SPORTTRACKS_CLIENT_ID, + 'client_secret': SPORTTRACKS_CLIENT_SECRET, + 'redirect_uri': SPORTTRACKS_REDIRECT_URI, + 'authorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", + 'content_type': 'application/json', + 'tokenname': 'sporttrackstoken', + 'refreshtokenname': 'sporttracksrefreshtoken', + 'expirydatename': 'sporttrackstokenexpirydate', + 'bearer_auth': False, + 'base_url': "https://api.sporttracks.mobi/oauth2/token", + 'scope': 'write', + } + + def get_name(self): + return "SportTracks" + + def get_shortname(self): + return "sporttracks" + + def open(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).open(*args, **kwargs) + + def createworkoutdata(self, w, *args, **kwargs): + timezone = pytz.timezone(w.timezone) + + filename = w.csvfilename + try: + row = rowingdata(csvfile=filename) + except: # pragma: no cover + return {} + + try: + averagehr = int(row.df[' HRCur (bpm)'].mean()) + maxhr = int(row.df[' HRCur (bpm)'].max()) + except KeyError: # pragma: no cover + averagehr = 0 + maxhr = 0 + + try: + duration = w.duration.hour*3600 + duration += w.duration.minute*60 + duration += w.duration.second + duration += +1.0e-6*w.duration.microsecond + except AttributeError: # pragma: no cover + return {} + + t = row.df.loc[:, 'TimeStamp (sec)'].values - \ + row.df.loc[:, 'TimeStamp (sec)'].iloc[0] + try: + t[0] = t[1] + except IndexError: # pragma: no cover + return {} + + d = row.df.loc[:, 'cum_dist'].values + d[0] = d[1] + t = t.astype(int) + d = d.astype(int) + spm = row.df[' Cadence (stokes/min)'].astype(int).values + spm[0] = spm[1] + hr = row.df[' HRCur (bpm)'].astype(int).values + + haslatlon = 1 + + try: + lat = row.df[' latitude'].values + lon = row.df[' longitude'].values + if not lat.std() and not lon.std(): # pragma: no cover + haslatlon = 0 + except KeyError: + haslatlon = 0 + + haspower = 1 + try: + power = row.df[' Power (watts)'].astype(int).values + except KeyError: # pragma: no cover + haspower = 0 + + locdata = [] + hrdata = [] + spmdata = [] + distancedata = [] + powerdata = [] + + t = t.tolist() + hr = hr.tolist() + d = d.tolist() + spm = spm.tolist() + if haslatlon: + lat = lat.tolist() + lon = lon.tolist() + power = power.tolist() + + for i in range(len(t)): + hrdata.append(t[i]) + hrdata.append(hr[i]) + distancedata.append(t[i]) + distancedata.append(d[i]) + spmdata.append(t[i]) + spmdata.append(spm[i]) + if haslatlon: + locdata.append(t[i]) + locdata.append([lat[i], lon[i]]) + if haspower: + powerdata.append(t[i]) + powerdata.append(power[i]) + + try: + w.notes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' + except TypeError: + w.notes = 'from '+w.workoutsource+' via rowsandall.com' + + st = w.startdatetime.astimezone(timezone) + st = st.replace(microsecond=0) + + data = { + "type": "Rowing", + "name": w.name, + "start_time": st.isoformat(), + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "avg_heartrate": averagehr, + "max_heartrate": maxhr, + "distance": distancedata, + "cadence": spmdata, + "heartrate": hrdata, + } + + if haslatlon: + data = { + "type": "Rowing", + "name": w.name, + "start_time": st.isoformat(), + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "avg_heartrate": averagehr, + "max_heartrate": maxhr, + "location": locdata, + "distance": distancedata, + "cadence": spmdata, + "heartrate": hrdata, + } + + + if haspower: + data['power'] = powerdata + + return data + + + def workout_export(self, workout, *args, **kwargs) -> str: + thetoken = self.open() + stid = "0" + # ready to upload. Hurray + + if not(is_workout_user(self.user, workout)): + return "0" + + authorizationstring = str('Bearer ' + thetoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + + data = self.createworkoutdata(workout) + + if not data: + return "0" + + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json" + _ = myqueue( + queue, + handle_sporttracks_sync, + workout.id, + url, + headers, + json.dumps(data, default=default)) + + return 1 + + def get_workouts(self, *args, **kwargs) -> int: + r = self.rower + workouts_json = self.get_workout_list_json(*args, **kwargs) + + stids = [int(getidfromuri(item['uri'])) + for item in workouts_json['items']] + + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + newids = [stid for stid in stids if stid not in knownstids] + for sporttracksid in newids: + id = self.get_workout(sporttracksid) + + return 1 + + + + def get_workout(self, id) -> int: + _ = self.open() + + r = self.rower + + + job = myqueue( + queue, + handle_sporttracks_workout_from_data, + self.user, + id, + 'sporttracks', + 'sporttracks' + ) + + return job.id + + def get_workout_list_json(self, *args, **kwargs) -> dict: + _ = self.open() + r = self.rower + + authorizationstring = str('Bearer ' + r.sporttrackstoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities" + res = requests.get(url, headers=headers) + + if (res.status_code != 200): + s = "Token doesn't exist. Need to authorize" + raise NoTokenError(s) + + return res.json() + + def get_workout_list(self, *args, **kwargs) -> list: + r = self.rower + workouts_json = self.get_workout_list_json(*args, **kwargs) + + workouts = [] + + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + for item in workouts_json['items']: + d = int(float(item['total_distance'])) + i = int(getidfromuri(item['uri'])) + if i in knownstids: # pragma: no cover + nnn = '' + else: + nnn = 'NEW' + n = item['name'] + ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) + s = item['start_time'] + r = item['type'] + keys = ['id', 'distance', 'duration', + 'starttime', 'rowtype', 'source', 'name', 'new'] + values = [i, d, ttot, s, r, None, n, nnn] + res = dict(zip(keys, values)) + workouts.append(res) + + return workouts + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + return super(SportTracksIntegration, self).make_authorization_url(*args, **kwargs) + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + return super(SportTracksIntegration, self).get_token(code, *args, **kwargs) + + + + def token_refresh(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).token_refresh(*args, **kwargs) + + diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py new file mode 100644 index 00000000..6344fd1c --- /dev/null +++ b/rowers/integrations/strava.py @@ -0,0 +1,346 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone +from rowingdata import rowingdata + +from rowers import mytypes + +from rowers.tasks import handle_strava_sync, fetch_strava_workout +from stravalib.exc import ActivityUploadFailed, TimeoutExceeded +from rowers.rower_rules import is_workout_user, ispromember +from rowers.utils import get_strava_stream + +from rowers.utils import myqueue, dologging +from rowers.imports import * + +import gzip +import time +import requests +import arrow +import datetime + +from rowsandall_app.settings import ( + STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, + SITE_URL +) +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +webhookverification = "kudos_to_rowing" +webhooklink = SITE_URL+'/rowers/strava/webhooks/' + +headers = {'Accept': 'application/json', + 'Api-Key': STRAVA_CLIENT_ID, + 'Content-Type': 'application/json', + 'user-agent': 'sanderroosendaal'} + +from json.decoder import JSONDecodeError +from rowers.dataprep import columndict + +def strava_establish_push(): # pragma: no cover + url = "https://www.strava.com/api/v3/push_subscriptions" + post_data = { + 'client_id': STRAVA_CLIENT_ID, + 'client_secret': STRAVA_CLIENT_SECRET, + 'callback_url': webhooklink, + 'verify_token': webhookverification, + } + + response = requests.post(url, data=post_data) + + return response.status_code + + +def strava_list_push(): # pragma: no cover + url = "https://www.strava.com/api/v3/push_subscriptions" + params = { + 'client_id': STRAVA_CLIENT_ID, + 'client_secret': STRAVA_CLIENT_SECRET, + + } + response = requests.get(url, params=params) + + if response.status_code == 200: + data = response.json() + return [w['id'] for w in data] + return [] + + +def strava_push_delete(id): # pragma: no cover + url = "https://www.strava.com/api/v3/push_subscriptions/{id}".format(id=id) + params = { + 'client_id': STRAVA_CLIENT_ID, + 'client_secret': STRAVA_CLIENT_SECRET, + } + response = requests.delete(url, json=params) + return response.status_code + + +class StravaIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(StravaIntegration, self).__init__(*args, **kwargs) + self.oauth_data = { + 'client_id': STRAVA_CLIENT_ID, + 'client_secret': STRAVA_CLIENT_SECRET, + 'redirect_uri': STRAVA_REDIRECT_URI, + 'autorization_uri': "https://www.strava.com/oauth/authorize", + 'content_type': 'application/json', + 'tokenname': 'stravatoken', + 'refreshtokenname': 'stravarefreshtoken', + 'expirydatename': 'stravatokenexpirydate', + 'bearer_auth': True, + 'base_url': "https://www.strava.com/oauth/token", + 'grant_type': 'refresh_token', + 'headers': headers, + 'scope': 'activity:write,activity:read_all', + } + + def get_token(self, code, *args, **kwargs): + return super(StravaIntegration, self).get_token(code, *args, **kwargs) + + def get_name(self): + return "Strava" + + def get_shortname(self): + return "strava" + + def open(self, *args, **kwargs): + dologging('strava_log.log','Getting token for user {id}'.format(id=self.rower.id)) + token = super(StravaIntegration, self).open(*args, **kwargs) + if self.rower.strava_owner_id == 0: + _ = self.set_strava_athlete_id() + + return token + + # createworkoutdata + def createworkoutdata(self, w, *args, **kwargs) -> str: + dozip = kwargs.get('dozip', True) + filename = w.csvfilename + try: + row = rowingdata(csvfile=filename) + except IOError: # pragma: no cover + data = dataprep.read_df_sql(w.id) + try: + datalength = len(data) + except AttributeError: + datalength = 0 + + if datalength != 0: + data.rename(columns=columndict, inplace=True) + _ = data.to_csv(w.csvfilename+'.gz', + index_label='index', + compression='gzip') + try: + row = rowingdata(csvfile=filename) + except IOError: + return '' + else: + return '' + + 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) + if dozip: + gzfilename = tcxfilename+'.gz' + with open(tcxfilename, 'rb') as inF: + s = inF.read() + with gzip.GzipFile(gzfilename, 'wb') as outF: + outF.write(s) + + try: + os.remove(tcxfilename) + except WindowError: # pragma: no cover + pass + + return gzfilename + + return tcxfilename + + + # workout_export + def workout_export(self, workout, *args, **kwargs) -> str: + description = kwargs.get('description','') + quick = kwargs.get('quick',False) + try: + _ = self.open() + except NoTokenError: + return 0 + + if (self.rower.stravatoken == '') or (self.rower.stravatoken is None): + raise NoTokenError("Your hovercraft is full of eels") + + if not (is_workout_user(self.user, workout)): + return 0 + + tcxfile = self.createworkoutdata(workout) + if not tcxfile: + return 0 + activity_type = self.rower.stravaexportas + if activity_type == 'match': + try: + activity_type = mytypes.stravamapping[workout.workouttype] + except KeyError: + activity_type = 'Rowing' + + _ = myqueue(queue, + handle_strava_sync, + self.rower.stravatoken, + workout.id, + tcxfile, workout.name, activity_type, + workout.notes) + + dologging('strava_export_log.log', 'Exporting as {t} from {w}'.format( + t=activity_type, w=workout.workouttype)) + + return 1 + + # get_workouts + def get_workouts(workout, *args, **kwargs) -> int: + return NotImplemented + + # get_workout + def get_workout(self, id) -> int: + try: + _ = self.open() + except NoTokenError: + return 0 + + csvfilename = 'media/{code}_{id}.csv'.format( + code=uuid4().hex[:16], id=id) + job = myqueue(queue, + fetch_strava_workout, + self.rower.stravatoken, + self.oauth_data, + id, + csvfilename, + self.user.id, + ) + + return job.id + + + # get_workout_list + def get_workout_list(self, *args, **kwargs) -> list: + limit_n = kwargs.get('limit_n',0) + + if (self.rower.stravatoken == '') or (self.rower.stravatoken is None): # pragma: no cover + raise NoTokenError + elif (self.rower.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > self.rower.stravatokenexpirydate): # pragma: no cover + raise NoTokenError + + # ready to fetch. Hurray + authorizationstring = str('Bearer ' + self.rower.stravatoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + + url = "https://www.strava.com/api/v3/athlete/activities" + + if limit_n == 0: + params = {} + else: # pragma: no cover + params = {'per_page': limit_n} + + res = requests.get(url, headers=headers, params=params) + + if (res.status_code != 200): # pragma: no cover + return [] + + workouts = [] + rower = self.rower + stravaids = [int(item['id']) for item in res.json()] + stravadata = [{ + 'id': int(item['id']), + 'elapsed_time':item['elapsed_time'], + 'start_date':item['start_date'], + } for item in res.json()] + + wfailed = Workout.objects.filter(user=rower, uploadedtostrava=-1) + + for w in wfailed: # pragma: no cover + for item in stravadata: + elapsed_time = item['elapsed_time'] + start_date = item['start_date'] + stravaid = item['id'] + if arrow.get(start_date) == arrow.get(w.startdatetime): + elapsed_td = datetime.timedelta(seconds=int(elapsed_time)) + elapsed_time = datetime.datetime.strptime( + str(elapsed_td), + "%H:%M:%S" + ) + if str(elapsed_time)[-7:] == str(w.duration)[-7:]: + w.uploadedtostrava = int(stravaid) + w.save() + + knownstravaids = uniqify([ + w.uploadedtostrava for w in Workout.objects.filter(user=self.rower) + ]) + + for item in res.json(): + d = int(float(item['distance'])) + i = item['id'] + if i in knownstravaids: # pragma: no cover + nnn = '' + else: + nnn = 'NEW' + n = item['name'] + ttot = str(datetime.timedelta( + seconds=int(float(item['elapsed_time'])))) + s = item['start_date'] + r = item['type'] + s2 = None + keys = ['id', 'distance', 'duration', + 'starttime', 'rowtype', 'source', 'name', 'new'] + values = [i, d, ttot, s, r, s2, n, nnn] + res2 = dict(zip(keys, values)) + workouts.append(res2) + + return workouts + + + # make_authorization_url + def make_authorization_url(self, *args, **kwargs): + params = {"client_id": STRAVA_CLIENT_ID, + "response_type": "code", + "redirect_uri": STRAVA_REDIRECT_URI, + "scope": "activity:write,activity:read_all"} + + url = "https://www.strava.com/oauth/authorize?" + \ + urllib.parse.urlencode(params) + + return url + + # token_refresh + def token_refresh(self, *args, **kwargs): + return super(StravaIntegration, self).token_refresh(*args, **kwargs) + + def set_strava_athlete_id(self, *args, **kwargs): + r = self.rower + if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover + s = "Token doesn't exist. Need to authorize" + return custom_exception_handler(401, s) + elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > r.stravatokenexpirydate): + _ = self.open() + + authorizationstring = str('Bearer ' + r.stravatoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://www.strava.com/api/v3/athlete" + + response = requests.get(url, headers=headers, params={}) + + if response.status_code == 200: # pragma: no cover + r.strava_owner_id = response.json()['id'] + r.save() + return response.json()['id'] + + return 0 + + + diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py new file mode 100644 index 00000000..c816ee6a --- /dev/null +++ b/rowers/integrations/trainingpeaks.py @@ -0,0 +1,149 @@ +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 get_name(self): + return "TrainingPeaks" + + def get_shortname(self): + return "trainingpeaks" + + 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 + params = {"client_id": TP_CLIENT_KEY, + "response_type": "code", + "redirect_uri": TP_REDIRECT_URI, + "scope": "file:write", + } + url = TP_OAUTH_LOCATION+"oauth/authorize/?" + \ + urllib.parse.urlencode(params) + + return url + + + 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/interactiveplots.py b/rowers/interactiveplots.py index ff5d1464..8235fc74 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -17,7 +17,8 @@ import rowers.c2stuff as c2stuff import rowers.metrics as metrics import rowers.dataprep as dataprep from rowers.dataprep import rdata -import rowers.stravastuff as stravastuff +import rowers.utils as utils + from scipy.interpolate import griddata from scipy.signal import savgol_filter from scipy import optimize @@ -5401,7 +5402,7 @@ def interactive_flexchart_stacked(id, r, xparam='time', if metricsdicts[column]['maysmooth']: nrsteps = int(log2(r.usersmooth)) for i in range(nrsteps): - rowdata[column] = stravastuff.ewmovingaverage( + rowdata[column] = utils.ewmovingaverage( rowdata[column], 5) except KeyError: pass @@ -5767,7 +5768,7 @@ def interactive_flex_chart2(id, r, promember=0, if metricsdicts[column]['maysmooth']: nrsteps = int(log2(r.usersmooth)) for i in range(nrsteps): - rowdata[column] = stravastuff.ewmovingaverage( + rowdata[column] = utils.ewmovingaverage( rowdata[column], 5) except KeyError: pass diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index 33f3427a..593a4e9c 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -27,13 +27,8 @@ from rowingdata import rowingdata as rrdata import rowers.uploads as uploads -import rowers.polarstuff as polarstuff -import rowers.c2stuff as c2stuff -import rowers.rp3stuff as rp3stuff -import rowers.stravastuff as stravastuff -import rowers.nkstuff as nkstuff from rowers.opaque import encoder - +from rowers.integrations import * from rowers.rower_rules import user_is_not_basic, user_is_coachee from rowers.utils import dologging @@ -79,8 +74,9 @@ class Command(BaseCommand): def handle(self, *args, **options): # Polar try: - polar_available = polarstuff.get_polar_notifications() - _ = polarstuff.get_all_new_workouts(polar_available) + polarintegration = PolarIntegration(None) + + _ = polarintegration.get_workouts() except: # pragma: no cover exc_type, exc_value, exc_traceback = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_traceback) @@ -91,7 +87,9 @@ class Command(BaseCommand): rowers = Rower.objects.filter(c2_auto_import=True) for r in rowers: # pragma: no cover if user_is_not_basic(r.user) or user_is_coachee(r.user): - c2stuff.get_c2_workouts(r) + c2integration = C2Integration(r.user) + _ = c2integration.get_workouts() + except: # pragma: no cover exc_type, exc_value, exc_traceback = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_traceback) @@ -101,7 +99,8 @@ class Command(BaseCommand): rowers = Rower.objects.filter(rp3_auto_import=True) for r in rowers: # pragma: no cover if user_is_not_basic(r.user) or user_is_coachee(r.user): - _ = rp3stuff.get_rp3_workouts(r) + rp3_integration = RP3Integration(r.user) + _ = rp3_integration.get_workouts() except: # pragma: no cover exc_type, exc_value, exc_traceback = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_traceback) @@ -112,7 +111,8 @@ class Command(BaseCommand): for r in rowers: # pragma: no cover dologging("nklog.log","NK Auto import set for rower {id}".format(id=r.user.id)) if user_is_not_basic(r.user) or user_is_coachee(r.user): - _ = nkstuff.get_nk_workouts(r) + nk_integration = NKIntegration(r.user) + _ = nk_integration.get_workouts() except: # pragma: no cover exc_type, exc_value, exc_traceback = sys.exc_info() lines = traceback.format_exception(exc_type, exc_value, exc_traceback) diff --git a/rowers/nkstuff.py b/rowers/nkstuff.py deleted file mode 100644 index 1671c563..00000000 --- a/rowers/nkstuff.py +++ /dev/null @@ -1,284 +0,0 @@ -from requests.auth import HTTPBasicAuth -from rowers.nkimportutils import * -from rowers.imports import * -from rowers.tasks import handle_nk_async_workout -from rowsandall_app.settings import ( - NK_CLIENT_ID, NK_REDIRECT_URI, NK_CLIENT_SECRET, - SITE_URL, NK_API_LOCATION, NK_OAUTH_LOCATION, - UPLOAD_SERVICE_URL, UPLOAD_SERVICE_SECRET, -) -import gzip -import rowers.mytypes as mytypes -from rowers.utils import myqueue -from iso8601 import ParseError -from rowers.rower_rules import is_workout_user, ispromember - -import time -from time import strftime - -import requests - -from rowers.utils import dologging -from json.decoder import JSONDecodeError - -# https:#oauth-stage.nkrowlink.com/oauth/authorizegrant_type=authorization_code&response_type=code&client_id=rowsandall-staging&scope=read&state=fc8fc3d8-ce0a-443e-838a-1c06fb5317c6&redirect_uri=https%3A%2F%2Fdunav.ngrok.io%2Fnk_callback%2F -# https:#oauth-stage.nkrowlink.com/oauth/authorize?grant_type=authorization_code&response_type=code&client_id=rowsandall-staging&scope=read&state=1234&redirect_uri=https%3A%2F%2Fdev.rowsandall.com%2Fnk_callback - -from requests_oauthlib import OAuth2Session - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('low') - -oauth_data = { - 'client_id': NK_CLIENT_ID, - 'client_secret': NK_CLIENT_SECRET, - 'redirect_uri': NK_REDIRECT_URI, - 'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", - 'content_type': 'application/json', - 'tokenname': 'nktoken', - 'refreshtokenname': 'nkrefreshtoken', - 'expirydatename': 'nktokenexpirydate', - 'bearer_auth': True, - 'base_url': NK_OAUTH_LOCATION+"/oauth/token", - 'scope': 'read', -} - - -def get_token(code): # pragma: no cover - url = oauth_data['base_url'] - - post_data = {"client_id": oauth_data['client_id'], - "grant_type": "authorization_code", - "redirect_uri": oauth_data['redirect_uri'], - "code": code, - } - - response = requests.post(url, auth=HTTPBasicAuth(oauth_data['client_id'], oauth_data['client_secret']), - data=post_data) - - - if response.status_code != 200: - return [0, response.text, 0, 0] - - token_json = response.json() - - access_token = token_json['access_token'] - refresh_token = token_json['refresh_token'] - expires_in = token_json['expires_in'] - nk_owner_id = token_json['user_id'] - - return [access_token, expires_in, refresh_token, nk_owner_id] - - -def nk_open(user): - r = Rower.objects.get(user=user) - - if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover - raise NoTokenError("User has no token") - else: - if (timezone.now() > r.nktokenexpirydate): - - thetoken = rower_nk_token_refresh(user) - if thetoken is None: # pragma: no cover - raise NoTokenError("User has no token") - return thetoken - else: - thetoken = r.nktoken - - return thetoken - - -def get_nk_workouts(rower, do_async=True, before=0, after=0): - try: - _ = nk_open(rower.user) - except NoTokenError: # pragma: no cover - dologging("nklog.log","NK Token error for user {id}".format(id=rower.user.id)) - return 0 - - res = get_nk_workout_list(rower.user, before=before, after=after) - - if res.status_code != 200: # pragma: no cover - dologging('nklog.log','Status code {code}'.format(code=res.status_code)) - return 0 - - #dologging('nklog.log',json.dumps(res.json())) - nkids = [item['id'] for item in res.json()] - dologging('nklog.log',json.dumps(nkids)) - alldata = {} - for item in res.json(): - alldata[item['id']] = item - - knownnkids = [ - w.uploadedtonk for w in Workout.objects.filter(user=rower) - ] - - tombstones = [ - t.uploadedtonk for t in TombStone.objects.filter(user=rower) - ] - - parkedids = [] - try: - with open('nkblocked.json', 'r') as nkblocked: - jsondata = json.load(nkblocked) - parkedids = jsondata['ids'] - except (FileNotFoundError, JSONDecodeError): # pragma: no cover - pass - - knownnkids = uniqify(knownnkids+tombstones+parkedids) - newids = [nkid for nkid in nkids if nkid not in knownnkids] - newids2 = newids.copy() - dologging('nklog.log',json.dumps(newids)) - dologging('nklog.log','Nr of new IDs is {i}'.format(i=len(newids))) - #if len(newids)>0: - # s = 'Starting NK Auto Import for user {id}'.format(id=r.user.id) - # dologging('nklog.log', s) - # s = 'New NK IDs {newids} (user {id})'.format(newids=newids,id=rower.user.id) - # dologging('nklog.log', s) - #else: - # dologging('nklog.log','Newids is false') - - newparkedids = uniqify(newids+parkedids) - - with open('nkblocked.json', 'wt') as nkblocked: - data = {'ids': newparkedids} - json.dump(data, nkblocked) - - counter = 0 - #dologging('nklog.log','Ik ben hier') - for nkid in newids2: - dologging('nklog.log','Queueing {id}'.format(id=nkid)) - res = myqueue(queuehigh, - handle_nk_async_workout, - alldata, - rower.user.id, - rower.nktoken, - nkid, - counter, - rower.defaulttimezone - ) - counter += 1 - - return 1 - - -def do_refresh_token(refreshtoken): - post_data = {"grant_type": "refresh_token", - # "client_id":NK_CLIENT_ID, - "refresh_token": refreshtoken, - } - - url = oauth_data['base_url'] - - response = requests.post(url, data=post_data, auth=HTTPBasicAuth( - oauth_data['client_id'], oauth_data['client_secret'])) - - if response.status_code != 200: # pragma: no cover - return [0, 0, 0] - - token_json = response.json() - - access_token = token_json['access_token'] - refresh_token = token_json['refresh_token'] - expires_in = token_json['expires_in'] - - return access_token, expires_in, refresh_token - - -def rower_nk_token_refresh(user): - r = Rower.objects.get(user=user) - res = do_refresh_token(r.nkrefreshtoken) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+timedelta(seconds=expires_in) - - r.nktoken = access_token - r.nktokenexpirydate = expirydatetime - r.nkrefreshtoken = refresh_token - r.save() - - return r.nktoken - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - - -def get_nk_workout_list(user, fake=False, after=0, before=0): - r = Rower.objects.get(user=user) - - if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (r.nktokenexpirydate is None or - timezone.now()+timedelta(seconds=10) > r.nktokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - if not before: # pragma: no cover - before = arrow.now()+timedelta(days=1) - before = str(int(before.timestamp())*1000) - if not after: # pragma: no cover - after = arrow.now()-timedelta(days=7) - after = str(int(after.timestamp())*1000) - authorizationstring = str('Bearer ' + r.nktoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json', - } - - url = NK_API_LOCATION+"api/v1/sessions" - - params = { - 'after': after, - 'before': before, - } # start / end time - - s = requests.get(url, headers=headers, params=params) - - return s - -# - - -def get_workout(user, nkid, do_async=True, startdate='', enddate=''): - r = Rower.objects.get(user=user) - if (r.nktoken == '') or (r.nktoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s), 0 - elif (timezone.now() > r.nktokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s), 0 - - before = 0 - after = 0 - if startdate: # pragma: no cover - startdate = arrow.get(startdate) - after = str(int(startdate.timestamp())*1000) - if enddate: # pragma: no cover - enddate = arrow.get(enddate) - before = str(int(enddate.timestamp())*1000) - - res = get_nk_workout_list(r.user, before=before, after=after) - if res.status_code != 200: # pragma: no cover - # dologging('nklog.log','Status code {code}'.format(code=res.status_code)) - return 0 - alldata = {} - for item in res.json(): - alldata[item['id']] = item - - res = myqueue( - queuehigh, - handle_nk_async_workout, - alldata, - r.user.id, - r.nktoken, - nkid, - 0, - r.defaulttimezone, - ) - - return res diff --git a/rowers/ownapistuff.py b/rowers/ownapistuff.py index d222986d..cf81c148 100644 --- a/rowers/ownapistuff.py +++ b/rowers/ownapistuff.py @@ -15,7 +15,6 @@ import math from math import sin, cos, atan2, sqrt import urllib -import rowers.c2stuff as c2stuff # Django from django.http import HttpResponseRedirect, HttpResponse, JsonResponse diff --git a/rowers/polarstuff.py b/rowers/polarstuff.py deleted file mode 100644 index 79073f93..00000000 --- a/rowers/polarstuff.py +++ /dev/null @@ -1,495 +0,0 @@ -from rowers.rower_rules import ispromember -from stravalib.exc import ActivityUploadFailed, TimeoutExceeded -from rowers.models import Rower, Workout -import rowers.mytypes as mytypes -from rowers.utils import NoTokenError, custom_exception_handler -from rowers.utils import dologging -from rowsandall_app.settings import ( - POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET, UPLOAD_SERVICE_URL -) -import stravalib -from io import StringIO -from rowers.dataprep import columndict -import rowers.dataprep as dataprep -from rowers.tasks import handle_request_post -import pandas as pd -from rowingdata import rowingdata - -# All the functionality needed to connect to Strava - -# Python -import oauth2 as oauth -import cgi -import requests -import requests.auth -import json -from django.utils import timezone -from datetime import datetime -import numpy as np -from dateutil import parser -import time -import math -from math import sin, cos, atan2, sqrt -import os -import sys -import gzip -import base64 -import yaml -from uuid import uuid4 -from requests import ConnectionError -from json.decoder import JSONDecodeError - -# Django -from django.http import HttpResponseRedirect, HttpResponse, JsonResponse -from django.conf import settings -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.models import User -from django.contrib.auth.decorators import login_required -from django.urls import reverse, reverse_lazy - -from rowers.utils import myqueue -from rowers.opaque import encoder -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('high') - -# Project -# from .models import Profile - -baseurl = 'https://polaraccesslink.com/v3' - - -# Exchange access code for long-lived access token - -def get_token(code): - - post_data = {"grant_type": "authorization_code", - "code": code, - # "redirect_uri": POLAR_REDIRECT_URI, - } - - auth_string = '{id}:{secret}'.format( - id=POLAR_CLIENT_ID, - secret=POLAR_CLIENT_SECRET - ) - - try: - headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} - except TypeError: - headers = {'Authorization': 'Basic %s' % base64.b64encode( - bytes(auth_string, 'utf-8')).decode('utf-8')} - - dologging('polar.log', 'Getting token') - dologging('polar.log', post_data) - dologging('polar.log', auth_string) - - response = requests.post("https://polarremote.com/v2/oauth2/token", - data=post_data, - headers=headers) - - if response.status_code != 200: # pragma: no cover - dologging('polar.log', 'Getting token, got:') - dologging('polar.log', response.status_code) - dologging('polar.log', response.reason) - dologging('polar.log', response.text) - - try: - token_json = response.json() - thetoken = token_json['access_token'] - expires_in = token_json['expires_in'] - user_id = token_json['x_user_id'] - dologging('polar.log', response.status_code) - try: - dologging('polar.log', response.text) - except AttributeError: - pass - dologging('polar.log', token_json) - except (KeyError, JSONDecodeError) as e: # pragma: no cover - dologging('polar.log', e) - try: - dologging('polar.log', response.text) - except AttributeError: - pass - thetoken = 0 - expires_in = 0 - user_id = 0 - - return [thetoken, expires_in, user_id] - -# Make authorization URL including random string - - -def make_authorization_url(): # pragma: no cover - # Generate a random string for the state parameter - # Save it for use later to prevent xsrf attacks - # state = str(uuid4()) - - params = {"client_id": POLAR_CLIENT_ID, - "response_type": "code", - "redirect_uri": POLAR_REDIRECT_URI, - "scope": "write"} - import urllib - url = "https://flow.polar.com/oauth2/authorization" + \ - urllib.parse.urlencode(params) - - return HttpResponseRedirect(url) - - -def revoke_access(user): # pragma: no cover - headers = { - 'Authorization': 'Bearer {token}'.format(token=user.rower.polartoken) - } - - response = requests.delete('https://www.polaraccesslink.com/v3/users/{userid}'.format( - userid=user.rower.polaruserid - ), headers=headers) - - dologging('polar.log', response.text) - dologging('polar.log', response.reason) - - return 1 - - -def get_polar_notifications(): - url = baseurl+'/notifications' - # state = str(uuid4()) - auth_string = '{id}:{secret}'.format( - id=POLAR_CLIENT_ID, - secret=POLAR_CLIENT_SECRET - ) - - try: - headers = {'Authorization': 'Basic %s' % base64.b64encode(auth_string)} - except TypeError: - headers = {'Authorization': 'Basic %s' % base64.b64encode( - bytes(auth_string, 'utf-8')).decode('utf-8')} - - try: - response = requests.get(url, headers=headers) - except ConnectionError: # pragma: no cover - response = { - 'status_code': 400, - } - - available_data = [] - - try: - if response.status_code == 200: - available_data = response.json()['available-user-data'] - dologging('polar.log', available_data) - else: # pragma: no cover - dologging('polar.log', response.status_code) - dologging('polar.log', response.text) - except AttributeError: # pragma: no cover - try: - dologging('polar.log', response.text) - except AttributeError: - pass - pass - - return available_data - - -def get_all_new_workouts(available_data, testing=False): - for record in available_data: - dologging('polar.log', str(record)) - if testing: # pragma: no cover - print(record) - if record['data-type'] == 'EXERCISE': - try: - r = Rower.objects.get(polaruserid=record['user-id']) - u = r.user - if r.polar_auto_import and ispromember(u): - exercise_list = get_polar_workouts(u) - dologging('polar.log', exercise_list) - if testing: # pragma: no cover - print(exercise_list) - except Rower.DoesNotExist: # pragma: no cover - pass - - return 1 - - -def get_polar_workouts(user): - r = Rower.objects.get(user=user) - - exercise_list = [] - - if (r.polartoken == '') or (r.polartoken is None): - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh" - dologging('polar.log', s) - return custom_exception_handler(401, s) - else: - authorizationstring = str('Bearer ' + r.polartoken) - headers = {'Authorization': authorizationstring, - 'Accept': 'application/json'} - - headers2 = { - 'Authorization': authorizationstring, - } - - url = baseurl+'/users/{userid}/exercise-transactions'.format( - userid=r.polaruserid - ) - - response = requests.post(url, headers=headers) - dologging('polar.log', url) - dologging('polar.log', authorizationstring) - dologging('polar.log', str(response.status_code)) - - if response.status_code == 201: - transactionid = response.json()['transaction-id'] - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - - dologging('polar.log', url) - - response = requests.get(url, headers=headers) - if response.status_code == 200: - exerciseurls = response.json()['exercises'] - dologging('polar.log', exerciseurls) - for exerciseurl in exerciseurls: - response = requests.get(exerciseurl, headers=headers) - if response.status_code == 200: - exercise_dict = response.json() - tcxuri = exerciseurl+'/tcx' - response = requests.get(tcxuri, headers=headers2) - - if response.status_code == 200: - filename = 'media/mailbox_attachments/{code}_{id}.tcx'.format( - id=exercise_dict['id'], - code=uuid4().hex[:16] - ) - dologging('polar.log', filename) - - with open(filename, 'wb') as fop: - fop.write(response.content) - - workouttype = 'other' - try: - workouttype = mytypes.polaraccesslink_sports[ - exercise_dict['detailed-sport-info']] - except KeyError: # pragma: no cover - dologging( - 'polar.log', exercise_dict['detailed-sport-info']) - dologging('polar.log', workouttype) - try: - workouttype = mytypes.polarmappinginv[exercise_dict['sport'].lower( - )] - except KeyError: - dologging('polar.log', workouttype) - pass - - dologging('polar.log', workouttype) - - # post file to upload api - # TODO: add workouttype - uploadoptions = { - 'title': '', - 'workouttype': workouttype, - 'boattype': '1x', - 'user': user.id, - 'secret': settings.UPLOAD_SERVICE_SECRET, - 'file': filename, - 'title': '', - } - - url = settings.UPLOAD_SERVICE_URL - - dologging('polar.log', uploadoptions) - dologging('polar.log', url) - - _ = myqueue( - queuehigh, - handle_request_post, - url, - uploadoptions - ) - - dologging('polar.log', response.status_code) - if response.status_code != 200: # pragma: no cover - try: - dologging('polar.log', response.text) - except: - pass - try: - dologging('polar.log', response.json()) - except: - pass - - exercise_dict['filename'] = filename - else: # pragma: no cover - exercise_dict['filename'] = '' - - exercise_list.append(exercise_dict) - dologging('polar.log', str(exercise_dict)) - - # commit transaction - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - requests.put(url, headers=headers) - dologging( - 'polar.log', 'Committed transation at {url}'.format(url=url)) - - return exercise_list - - -def register_user(user, token): - r = Rower.objects.get(user=user) - if (r.polartoken == '') or (r.polartoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh" - return custom_exception_handler(401, s) - - authorizationstring = 'Bearer {token}'.format(token=token) - headers = { - 'Content-Type': 'application/xml', - 'Authorization': authorizationstring, - 'Accept': 'application/json' - } - - payload = { - "member-id": encoder.encode_hex(user.id) - } - - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': 'Bearer {token}'.format(token=token) - } - - dologging('polar.log', 'Registering user') - - response = requests.post( - 'https://www.polaraccesslink.com/v3/users', - json=payload, - headers=headers - ) - - if response.status_code not in [200, 201]: # pragma: no cover - # dologging('polar.log',url) - dologging('polar.log', headers) - dologging('polar.log', payload) - dologging('polar.log', response.status_code) - dologging('polar.log', response.content) - try: - dologging('polar.log', response.reason) - dologging('polar.log', response.text) - except KeyError: - pass - - return {} - - polar_user_data = response.json() - - return polar_user_data - - -def get_polar_user_info(user, physical=False): # pragma: no cover - r = Rower.objects.get(user=user) - if (r.polartoken == '') or (r.polartoken is None): - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): - s = "Token expired. Needs to refresh" - return custom_exception_handler(401, s) - - authorizationstring = str('Bearer ' + r.polartoken) - headers = { - 'Authorization': authorizationstring, - 'Accept': 'application/json' - } - - if not physical: - url = baseurl+'/users/{userid}'.format( - userid=r.polaruserid - ) - else: - url = 'https://www.polaraccesslink.com/v3/users/{userid}/physical-information-transactions/'.format( - userid=r.polaruserid - ) - - if physical: - response = requests.post(url, headers=headers) - else: - response = requests.get(url, headers=headers) - - return response - - -def get_polar_workout(user, id, transactionid): - - r = Rower.objects.get(user=user) - if (r.polartoken == '') or (r.polartoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.polartokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh" - return custom_exception_handler(401, s) - else: - authorizationstring = str('Bearer ' + r.polartoken) - headers = { - 'Authorization': authorizationstring, - 'Accept': 'application/json' - } - - url = baseurl+'/users/{userid}/exercise-transactions'.format( - userid=r.polaruserid - ) - - response = requests.post(url, headers=headers) - - if response.status_code == 201: - transactionid = response.json()['transaction-id'] - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - - response = requests.get(url, headers=headers) - if response.status_code == 200: - exerciseurls = response.json()['exercises'] - for exerciseurl in exerciseurls: - response = requests.get(exerciseurl, headers=headers) - if response.status_code == 200: - exercise_dict = response.json() - thisid = exercise_dict['id'] - if thisid == id: - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}' \ - '/exercises/{exerciseid}/tcx'.format( - userid=r.polaruserid, - transactionid=transactionid, - exerciseid=id) - authorizationstring = str('Bearer ' + r.polartoken) - headers2 = { - 'Authorization': authorizationstring, - } - - response = requests.get(url, headers=headers2) - - if response.status_code == 200: - result = response.content - # commit transaction - url = baseurl+'/users/{userid}/exercise-transactions/{transactionid}'.format( - transactionid=transactionid, - userid=r.polaruserid - ) - response = requests.put(url, headers=headers) - dologging( - 'polar.log', 'Committing transaction on {url}'.format(url=url)) - else: # pragma: no cover - result = None - - return result - - return None # pragma: no cover diff --git a/rowers/rp3stuff.py b/rowers/rp3stuff.py deleted file mode 100644 index b4eed2f6..00000000 --- a/rowers/rp3stuff.py +++ /dev/null @@ -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'] diff --git a/rowers/sporttracksstuff.py b/rowers/sporttracksstuff.py deleted file mode 100644 index 4b4f2dd8..00000000 --- a/rowers/sporttracksstuff.py +++ /dev/null @@ -1,510 +0,0 @@ -from rowers.tasks import handle_sporttracks_sync -from rowers.rower_rules import is_workout_user -import rowers.mytypes as mytypes -from rowsandall_app.settings import ( - C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, - STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, - SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, - SPORTTRACKS_REDIRECT_URI -) -import re -from rowers.imports import * -from rowers.utils import myqueue - -# All the functionality to connect to SportTracks - -import numpy - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('low') - - -oauth_data = { - 'client_id': SPORTTRACKS_CLIENT_ID, - 'client_secret': SPORTTRACKS_CLIENT_SECRET, - 'redirect_uri': SPORTTRACKS_REDIRECT_URI, - 'autorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", - 'content_type': 'application/json', - 'tokenname': 'sporttrackstoken', - 'refreshtokenname': 'sporttracksrefreshtoken', - 'expirydatename': 'sporttrackstokenexpirydate', - 'bearer_auth': False, - 'base_url': "https://api.sporttracks.mobi/oauth2/token", - 'scope': 'write', -} - -# Checks if user has SportTracks token, renews them if they are expired - - -def sporttracks_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 ST access code for long-lived ST access token - - -def get_token(code): - return imports_get_token(code, oauth_data) - -# Make authorization URL including random string - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - -# This is token refresh. Looks for tokens in our database, then refreshes - - -def rower_sporttracks_token_refresh(user): # pragma: no cover - r = Rower.objects.get(user=user) - res = do_refresh_token(r.sporttracksrefreshtoken) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+timedelta(seconds=expires_in) - - r = Rower.objects.get(user=user) - r.sporttrackstoken = access_token - r.tokenexpirydate = expirydatetime - r.sporttracksrefreshtoken = refresh_token - - r.save() - - return r.sporttrackstoken - -# Get list of workouts available on SportTracks - - -def get_sporttracks_workout_list(user): - r = Rower.objects.get(user=user) - if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (timezone.now() > r.sporttrackstokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.sporttrackstoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://api.sporttracks.mobi/api/v2/fitnessActivities" - s = requests.get(url, headers=headers) - - return s - -# Get workout summary data by SportTracks ID - - -def get_workout(user, sporttracksid, do_async=False): - r = Rower.objects.get(user=user) - if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): # pragma: no cover - return custom_exception_handler(401, s) - s = "Token doesn't exist. Need to authorize" - elif (timezone.now() > r.sporttrackstokenexpirydate): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.sporttrackstoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \ - str(sporttracksid) - s = requests.get(url, headers=headers) - - data = s.json() - - strokedata = pd.DataFrame.from_dict({ - key: pd.Series(value, dtype='object') for key, value in data.items() - }) - - id, message = add_workout_from_data( - user, - sporttracksid, data, - strokedata, - source='sporttracks', - workoutsource='sporttracks') - - return id - -# Create Workout Data for upload to SportTracks - - -def createsporttracksworkoutdata(w): - timezone = pytz.timezone(w.timezone) - - filename = w.csvfilename - try: - row = rowingdata(csvfile=filename) - except: # pragma: no cover - return 0 - - try: - averagehr = int(row.df[' HRCur (bpm)'].mean()) - maxhr = int(row.df[' HRCur (bpm)'].max()) - except KeyError: # pragma: no cover - averagehr = 0 - maxhr = 0 - - try: - duration = w.duration.hour*3600 - duration += w.duration.minute*60 - duration += w.duration.second - duration += +1.0e-6*w.duration.microsecond - except AttributeError: # pragma: no cover - return 0 - - # adding diff, trying to see if this is valid - # t = row.df.loc[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)'] - t = row.df.loc[:, 'TimeStamp (sec)'].values - \ - row.df.loc[:, 'TimeStamp (sec)'].iloc[0] - try: - t[0] = t[1] - except IndexError: # pragma: no cover - return 0 - - d = row.df.loc[:, 'cum_dist'].values - d[0] = d[1] - t = t.astype(int) - d = d.astype(int) - spm = row.df[' Cadence (stokes/min)'].astype(int).values - spm[0] = spm[1] - hr = row.df[' HRCur (bpm)'].astype(int).values - - haslatlon = 1 - - try: - lat = row.df[' latitude'].values - lon = row.df[' longitude'].values - if not lat.std() and not lon.std(): # pragma: no cover - haslatlon = 0 - except KeyError: - haslatlon = 0 - - haspower = 1 - try: - power = row.df[' Power (watts)'].astype(int).values - except KeyError: # pragma: no cover - haspower = 0 - - locdata = [] - hrdata = [] - spmdata = [] - distancedata = [] - powerdata = [] - - t = t.tolist() - hr = hr.tolist() - d = d.tolist() - spm = spm.tolist() - if haslatlon: - lat = lat.tolist() - lon = lon.tolist() - power = power.tolist() - - for i in range(len(t)): - hrdata.append(t[i]) - hrdata.append(hr[i]) - distancedata.append(t[i]) - distancedata.append(d[i]) - spmdata.append(t[i]) - spmdata.append(spm[i]) - if haslatlon: - locdata.append(t[i]) - locdata.append([lat[i], lon[i]]) - if haspower: - powerdata.append(t[i]) - powerdata.append(power[i]) - - try: - w.notes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' - except TypeError: - w.notes = 'from '+w.workoutsource+' via rowsandall.com' - - st = w.startdatetime.astimezone(timezone) - st = st.replace(microsecond=0) - - if haslatlon: - data = { - "type": "Rowing", - "name": w.name, - "start_time": st.isoformat(), - "total_distance": int(w.distance), - "duration": duration, - "notes": w.notes, - "avg_heartrate": averagehr, - "max_heartrate": maxhr, - "location": locdata, - "distance": distancedata, - "cadence": spmdata, - "heartrate": hrdata, - } - else: - data = { - "type": "Rowing", - "name": w.name, - "start_time": st.isoformat(), - "total_distance": int(w.distance), - "duration": duration, - "notes": w.notes, - "avg_heartrate": averagehr, - "max_heartrate": maxhr, - "distance": distancedata, - "cadence": spmdata, - "heartrate": hrdata, - } - - if haspower: - data['power'] = powerdata - - return data - -# Obtain SportTracks Workout ID from the response returned on successful -# upload - - -def getidfromresponse(response): # pragma: no cover - t = response.json() - uri = t['uris'][0] - regex = '.*?sporttracks\.mobi\/api\/v2\/fitnessActivities/(\d+)\.json$' - m = re.compile(regex).match(uri).group(1) - - id = int(m) - - return int(id) - - -def default(o): # pragma: no cover - if isinstance(o, numpy.int64): - return int(o) - raise TypeError - - -def workout_sporttracks_upload(user, w, asynchron=False): # pragma: no cover - message = "Uploading to SportTracks" - stid = 0 - # ready to upload. Hurray - - thetoken = sporttracks_open(user) - - if (is_workout_user(user, w)): - data = createsporttracksworkoutdata(w) - if not data: - message = "Data error" - stid = 0 - return message, stid - - authorizationstring = str('Bearer ' + thetoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - - url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json" - if asynchron: - _ = myqueue(queue, handle_sporttracks_sync, - w.id, url, headers, json.dumps(data, default=default)) - return "Asynchronous sync", 0 - - response = requests.post(url, headers=headers, - data=json.dumps(data, default=default)) - - # check for duplicate error first - if (response.status_code == 409): - message = "Duplicate error" - w.uploadedtosporttracks = -1 - stid = -1 - w.save() - return message, stid - elif (response.status_code == 201 or response.status_code == 200): - s = response.json() - stid = getidfromresponse(response) - w.uploadedtosporttracks = stid - w.save() - return 'Successfully synced to SportTracks', stid - else: - s = response - message = "Something went wrong in workout_sporttracks_upload_view: %s" % s.reason - stid = 0 - return message, stid - - else: - message = "You are not authorized to upload this workout" - stid = 0 - return message, stid - - return message, stid - -# Create workout from SportTracks Data, which are slightly different -# than Strava or Concept2 data - - -def add_workout_from_data(user, importid, data, strokedata, source='sporttracks', - workoutsource='sporttracks'): - try: - workouttype = data['type'] - except KeyError: # pragma: no cover - workouttype = 'other' - - if workouttype not in [x[0] for x in Workout.workouttypes]: - workouttype = 'other' - try: - comments = data['comments'] - except: - comments = '' - - r = Rower.objects.get(user=user) - try: - rowdatetime = iso8601.parse_date(data['start_time']) - except iso8601.ParseError: # pragma: no cover - try: - rowdatetime = datetime.datetime.strptime( - data['start_time'], "%Y-%m-%d %H:%M:%S") - rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - except: - try: - rowdatetime = dateutil.parser.parse(data['start_time']) - rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - except: - rowdatetime = datetime.datetime.strptime( - data['date'], "%Y-%m-%d %H:%M:%S") - rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - starttimeunix = arrow.get(rowdatetime).timestamp() - - try: - title = data['name'] - except: # pragma: no cover - title = "Imported data" - - try: - res = splitstdata(data['distance']) - distance = res[1] - times_distance = res[0] - except KeyError: # pragma: no cover - try: - res = splitstdata(data['heartrate']) - times_distance = res[0] - distance = 0*times_distance - except KeyError: - return (0, "No distance or heart rate data in the workout") - - try: - locs = data['location'] - - res = splitstdata(locs) - times_location = res[0] - latlong = res[1] - latcoord = [] - loncoord = [] - - for coord in latlong: - lat = coord[0] - lon = coord[1] - latcoord.append(lat) - loncoord.append(lon) - except: - times_location = times_distance - latcoord = np.zeros(len(times_distance)) - loncoord = np.zeros(len(times_distance)) - if workouttype in mytypes.otwtypes: # pragma: no cover - workouttype = 'rower' - - try: - res = splitstdata(data['cadence']) - times_spm = res[0] - spm = res[1] - except KeyError: # pragma: no cover - times_spm = times_distance - spm = 0*times_distance - - try: - res = splitstdata(data['heartrate']) - hr = res[1] - times_hr = res[0] - except KeyError: - times_hr = times_distance - hr = 0*times_distance - - # create data series and remove duplicates - distseries = pd.Series(distance, index=times_distance) - distseries = distseries.groupby(distseries.index).first() - latseries = pd.Series(latcoord, index=times_location) - latseries = latseries.groupby(latseries.index).first() - lonseries = pd.Series(loncoord, index=times_location) - lonseries = lonseries.groupby(lonseries.index).first() - spmseries = pd.Series(spm, index=times_spm) - spmseries = spmseries.groupby(spmseries.index).first() - hrseries = pd.Series(hr, index=times_hr) - hrseries = hrseries.groupby(hrseries.index).first() - - # Create dicts and big dataframe - d = { - ' Horizontal (meters)': distseries, - ' latitude': latseries, - ' longitude': lonseries, - ' Cadence (stokes/min)': spmseries, - ' HRCur (bpm)': hrseries, - } - - df = pd.DataFrame(d) - - df = df.groupby(level=0).last() - - cum_time = df.index.values - df[' ElapsedTime (sec)'] = cum_time - - velo = df[' Horizontal (meters)'].diff()/df[' ElapsedTime (sec)'].diff() - - df[' Power (watts)'] = 0.0*velo - - nr_rows = len(velo.values) - - df[' DriveLength (meters)'] = np.zeros(nr_rows) - df[' StrokeDistance (meters)'] = np.zeros(nr_rows) - df[' DriveTime (ms)'] = np.zeros(nr_rows) - df[' StrokeRecoveryTime (ms)'] = np.zeros(nr_rows) - df[' AverageDriveForce (lbs)'] = np.zeros(nr_rows) - df[' PeakDriveForce (lbs)'] = np.zeros(nr_rows) - df[' lapIdx'] = np.zeros(nr_rows) - - unixtime = cum_time+starttimeunix - unixtime[0] = starttimeunix - - df['TimeStamp (sec)'] = unixtime - - dt = np.diff(cum_time).mean() - wsize = round(5./dt) - - velo2 = ewmovingaverage(velo, wsize) - - df[' Stroke500mPace (sec/500m)'] = 500./velo2 - - df = df.fillna(0) - - df.sort_values(by='TimeStamp (sec)', ascending=True) - -# csvfilename ='media/Import_'+str(importid)+'.csv' - csvfilename = 'media/{code}_{importid}.csv'.format( - importid=importid, - code=uuid4().hex[:16] - ) - - res = df.to_csv(csvfilename+'.gz', index_label='index', - compression='gzip') - - id, message = dataprep.save_workout_database(csvfilename, r, - workouttype=workouttype, - title=title, - notes=comments, - dosmooth=r.dosmooth, - workoutsource='sporttracks') - - return (id, message) diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py deleted file mode 100644 index 4eab0961..00000000 --- a/rowers/stravastuff.py +++ /dev/null @@ -1,404 +0,0 @@ -from rowers.tasks import handle_strava_sync, fetch_strava_workout -from stravalib.exc import ActivityUploadFailed, TimeoutExceeded -from rowers.rower_rules import is_workout_user, ispromember -from rowers.utils import get_strava_stream -from rowers.utils import dologging -from rowers.imports import * -from rowsandall_app.settings import ( - C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, - STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, - SITE_URL -) -import gzip -import rowers.mytypes as mytypes -from rowers.utils import myqueue -from iso8601 import ParseError -import stravalib -from rowers.dataprep import columndict - -# All the functionality needed to connect to Strava -from scipy import optimize -from scipy.signal import savgol_filter -import time -from time import strftime - - -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('low') - - -try: - from json.decoder import JSONDecodeError -except ImportError: # pragma: no cover - JSONDecodeError = ValueError - - -webhookverification = "kudos_to_rowing" -webhooklink = SITE_URL+'/rowers/strava/webhooks/' - -headers = {'Accept': 'application/json', - 'Api-Key': STRAVA_CLIENT_ID, - 'Content-Type': 'application/json', - 'user-agent': 'sanderroosendaal'} - - -oauth_data = { - 'client_id': STRAVA_CLIENT_ID, - 'client_secret': STRAVA_CLIENT_SECRET, - 'redirect_uri': STRAVA_REDIRECT_URI, - 'autorization_uri': "https://www.strava.com/oauth/authorize", - 'content_type': 'application/json', - 'tokenname': 'stravatoken', - 'refreshtokenname': 'stravarefreshtoken', - 'expirydatename': 'stravatokenexpirydate', - 'bearer_auth': True, - 'base_url': "https://www.strava.com/oauth/token", - 'grant_type': 'refresh_token', - 'headers': headers, - 'scope': 'activity:write,activity:read_all', -} - - -# Exchange access code for long-lived access token -def get_token(code): - return imports_get_token(code, oauth_data) - - -def strava_open(user): - t = time.localtime() - timestamp = time.strftime('%b-%d-%Y_%H%M', t) - with open('strava_open.log', 'a') as f: - f.write('\n') - f.write(timestamp) - f.write(' ') - f.write('Getting token for user ') - f.write(str(user.rower.id)) - f.write(' token expiry ') - f.write(str(user.rower.stravatokenexpirydate)) - f.write(' ') - f.write(json.dumps(oauth_data)) - f.write('\n') - token = imports_open(user, oauth_data) - if user.rower.strava_owner_id == 0: # pragma: no cover - _ = set_strava_athlete_id(user) - return token - - -def do_refresh_token(refreshtoken): - return imports_do_refresh_token(refreshtoken, oauth_data) - - -def rower_strava_token_refresh(user): - r = Rower.objects.get(user=user) - res = do_refresh_token(r.stravarefreshtoken) - access_token = res[0] - expires_in = res[1] - refresh_token = res[2] - expirydatetime = timezone.now()+timedelta(seconds=expires_in) - - r.stravatoken = access_token - r.stravatokenexpirydate = expirydatetime - r.stravarefreshtoken = refresh_token - r.save() - - return r.stravatoken - -# Make authorization URL including random string - - -def make_authorization_url(request): # pragma: no cover - return imports_make_authorization_url(oauth_data) - - -def strava_establish_push(): # pragma: no cover - url = "https://www.strava.com/api/v3/push_subscriptions" - post_data = { - 'client_id': STRAVA_CLIENT_ID, - 'client_secret': STRAVA_CLIENT_SECRET, - 'callback_url': webhooklink, - 'verify_token': webhookverification, - } - - response = requests.post(url, data=post_data) - - return response.status_code - - -def strava_list_push(): # pragma: no cover - url = "https://www.strava.com/api/v3/push_subscriptions" - params = { - 'client_id': STRAVA_CLIENT_ID, - 'client_secret': STRAVA_CLIENT_SECRET, - - } - response = requests.get(url, params=params) - - if response.status_code == 200: - data = response.json() - return [w['id'] for w in data] - return [] - - -def strava_push_delete(id): # pragma: no cover - url = "https://www.strava.com/api/v3/push_subscriptions/{id}".format(id=id) - params = { - 'client_id': STRAVA_CLIENT_ID, - 'client_secret': STRAVA_CLIENT_SECRET, - } - response = requests.delete(url, json=params) - return response.status_code - - -def set_strava_athlete_id(user): - r = Rower.objects.get(user=user) - if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif (r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > r.stravatokenexpirydate): - _ = imports_open(user, oauth_data) - - authorizationstring = str('Bearer ' + r.stravatoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - url = "https://www.strava.com/api/v3/athlete" - - response = requests.get(url, headers=headers, params={}) - - if response.status_code == 200: # pragma: no cover - r.strava_owner_id = response.json()['id'] - r.save() - return response.json()['id'] - - return 0 - - -# Get list of workouts available on Strava -def get_strava_workout_list(user, limit_n=0): - r = Rower.objects.get(user=user) - - if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover - s = "Token doesn't exist. Need to authorize" - return custom_exception_handler(401, s) - elif ( - r.stravatokenexpirydate is None or timezone.now()+timedelta(seconds=3599) > r.stravatokenexpirydate - ): # pragma: no cover - s = "Token expired. Needs to refresh." - return custom_exception_handler(401, s) - else: - # ready to fetch. Hurray - authorizationstring = str('Bearer ' + r.stravatoken) - headers = {'Authorization': authorizationstring, - 'user-agent': 'sanderroosendaal', - 'Content-Type': 'application/json'} - - url = "https://www.strava.com/api/v3/athlete/activities" - - if limit_n == 0: - params = {} - else: # pragma: no cover - params = {'per_page': limit_n} - - s = requests.get(url, headers=headers, params=params) - - return s - - -def async_get_workout(user, stravaid): - try: - _ = strava_open(user) - except NoTokenError: # pragma: no cover - return 0 - - csvfilename = 'media/{code}_{stravaid}.csv'.format( - code=uuid4().hex[:16], stravaid=stravaid) - job = myqueue(queue, - fetch_strava_workout, - user.rower.stravatoken, - oauth_data, - stravaid, - csvfilename, - user.id, - ) - return job - -# Get a Strava workout summary data and stroke data by ID - - -def get_workout(user, stravaid, do_async=True): - return async_get_workout(user, stravaid) - - -# Generate Workout data for Strava (a TCX file) -def createstravaworkoutdata(w, dozip=True): - filename = w.csvfilename - try: - row = rowingdata(csvfile=filename) - except IOError: # pragma: no cover - data = dataprep.read_df_sql(w.id) - try: - datalength = len(data) - except AttributeError: - datalength = 0 - - if datalength != 0: - data.rename(columns=columndict, inplace=True) - _ = data.to_csv(w.csvfilename+'.gz', - index_label='index', - compression='gzip') - try: - row = rowingdata(csvfile=filename) - except IOError: - return '', 'Error - could not find rowing data' - else: - return '', 'Error - could not find rowing data' - - 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) - if dozip: - gzfilename = tcxfilename+'.gz' - with open(tcxfilename, 'rb') as inF: - s = inF.read() - with gzip.GzipFile(gzfilename, 'wb') as outF: - outF.write(s) - - try: - os.remove(tcxfilename) - except WindowError: # pragma: no cover - pass - - return gzfilename, "" - - return tcxfilename, "" - - -# Upload the TCX file to Strava and set the workout activity type -# to rowing on Strava -def handle_stravaexport(f2, workoutname, stravatoken, description='', - activity_type='Rowing', quick=False, asynchron=False): - # w = Workout.objects.get(id=workoutid) - client = stravalib.Client(access_token=stravatoken) - - act = client.upload_activity(f2, 'tcx.gz', name=workoutname) - - try: - if quick: # pragma: no cover - res = act.wait(poll_interval=2.0, timeout=10) - message = 'Workout successfully synchronized to Strava' - else: - res = act.wait(poll_interval=5.0, timeout=30) - message = 'Workout successfully synchronized to Strava' - except: # pragma: no cover - res = 0 - message = 'Strava upload timed out' - - # description doesn't work yet. Have to wait for stravalib to update - if res: - try: - act = client.update_activity( - res.id, activity_type=activity_type, description=description, device_name='Rowsandall.com') - except TypeError: # pragma: no cover - act = client.update_activity( - res.id, activity_type=activity_type, description=description) - else: # pragma: no cover - message = 'Strava activity update timed out.' - return (0, message) - - return (res.id, message) - - -def workout_strava_upload(user, w, quick=False, asynchron=True): - try: - _ = strava_open(user) - except NoTokenError: # pragma: no cover - return "Please connect to Strava first", 0 - - message = "Uploading to Strava" - stravaid = -1 - r = Rower.objects.get(user=user) - res = -1 - if (r.stravatoken == '') or (r.stravatoken is None): # pragma: no cover - raise NoTokenError("Your hovercraft is full of eels") - - if (is_workout_user(user, w)): - if asynchron: - tcxfile, tcxmesg = createstravaworkoutdata(w) - if not tcxfile: # pragma: no cover - return "Failed to create workout data", 0 - activity_type = r.stravaexportas - if r.stravaexportas == 'match': - try: - activity_type = mytypes.stravamapping[w.workouttype] - except KeyError: # pragma: no cover - activity_type = 'Rowing' - _ = myqueue(queue, - handle_strava_sync, - r.stravatoken, - w.id, - tcxfile, w.name, activity_type, - w.notes) - dologging('strava_export_log.log', 'Exporting as {t} from {w}'.format( - t=activity_type, w=w.workouttype)) - return "Asynchronous sync", -1 - try: - tcxfile, tcxmesg = createstravaworkoutdata(w) - if tcxfile: - activity_type = r.stravaexportas - if r.stravaexportas == 'match': - try: - activity_type = mytypes.stravamapping[w.workouttype] - except KeyError: # pragma: no cover - activity_type = 'Rowing' - - with open(tcxfile, 'rb') as f: - try: - description = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' - except TypeError: - description = ' via rowsandall.com' - res, mes = handle_stravaexport( - f, w.name, - r.stravatoken, - description=description, - activity_type=activity_type, quick=quick, asynchron=asynchron) - if res == 0: # pragma: no cover - message = mes - w.uploadedtostrava = -1 - stravaid = -1 - w.save() - try: - os.remove(tcxfile) - except WindowsError: - pass - return message, stravaid - - w.uploadedtostrava = res - w.save() - try: - os.remove(tcxfile) - except WindowsError: # pragma: no cover - pass - message = mes - stravaid = res - return message, stravaid - else: # pragma: no cover - message = "Strava TCX data error "+tcxmesg - w.uploadedtostrava = -1 - stravaid = -1 - w.save() - return message, stravaid - - except ActivityUploadFailed as e: # pragma: no cover - message = "Strava Upload error: %s" % e - w.uploadedtostrava = -1 - stravaid = -1 - w.save() - os.remove(tcxfile) - return message, stravaid - return message, stravaid # pragma: no cover diff --git a/rowers/tasks.py b/rowers/tasks.py index 5c318741..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] @@ -447,6 +521,189 @@ def handle_c2_sync(workoutid, url, headers, data, debug=False, **kwargs): return 1 +def splitstdata(lijst): + t = [] + latlong = [] + while len(lijst) >= 2: + t.append(lijst[0]) + latlong.append(lijst[1]) + lijst = lijst[2:] + + return [np.array(t), np.array(latlong)] + +@app.task +def handle_sporttracks_workout_from_data(user, importid, source, + workoutsource, debug=False, **kwargs): + + r = user.rower + authorizationstring = str('Bearer ' + r.sporttrackstoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \ + str(importid) + s = requests.get(url, headers=headers) + + data = s.json() + + strokedata = pd.DataFrame.from_dict({ + key: pd.Series(value, dtype='object') for key, value in data.items() + }) + + try: + workouttype = data['type'] + except KeyError: # pragma: no cover + workouttype = 'other' + + if workouttype not in [x[0] for x in Workout.workouttypes]: + workouttype = 'other' + try: + comments = data['comments'] + except: + comments = '' + + r = Rower.objects.get(user=user) + rowdatetime = iso8601.parse_date(data['start_time']) + starttimeunix = arrow.get(rowdatetime).timestamp() + + try: + title = data['name'] + except: # pragma: no cover + title = "Imported data" + + try: + res = splitstdata(data['distance']) + distance = res[1] + times_distance = res[0] + except KeyError: # pragma: no cover + try: + res = splitstdata(data['heartrate']) + times_distance = res[0] + distance = 0*times_distance + except KeyError: + return (0, "No distance or heart rate data in the workout") + + try: + locs = data['location'] + + res = splitstdata(locs) + times_location = res[0] + latlong = res[1] + latcoord = [] + loncoord = [] + + for coord in latlong: + lat = coord[0] + lon = coord[1] + latcoord.append(lat) + loncoord.append(lon) + except: + times_location = times_distance + latcoord = np.zeros(len(times_distance)) + loncoord = np.zeros(len(times_distance)) + if workouttype in mytypes.otwtypes: # pragma: no cover + workouttype = 'rower' + + try: + res = splitstdata(data['cadence']) + times_spm = res[0] + spm = res[1] + except KeyError: # pragma: no cover + times_spm = times_distance + spm = 0*times_distance + + try: + res = splitstdata(data['heartrate']) + hr = res[1] + times_hr = res[0] + except KeyError: + times_hr = times_distance + hr = 0*times_distance + + # create data series and remove duplicates + distseries = pd.Series(distance, index=times_distance) + distseries = distseries.groupby(distseries.index).first() + latseries = pd.Series(latcoord, index=times_location) + latseries = latseries.groupby(latseries.index).first() + lonseries = pd.Series(loncoord, index=times_location) + lonseries = lonseries.groupby(lonseries.index).first() + spmseries = pd.Series(spm, index=times_spm) + spmseries = spmseries.groupby(spmseries.index).first() + hrseries = pd.Series(hr, index=times_hr) + hrseries = hrseries.groupby(hrseries.index).first() + + # Create dicts and big dataframe + d = { + ' Horizontal (meters)': distseries, + ' latitude': latseries, + ' longitude': lonseries, + ' Cadence (stokes/min)': spmseries, + ' HRCur (bpm)': hrseries, + } + + df = pd.DataFrame(d) + + df = df.groupby(level=0).last() + + cum_time = df.index.values + df[' ElapsedTime (sec)'] = cum_time + + velo = df[' Horizontal (meters)'].diff()/df[' ElapsedTime (sec)'].diff() + + df[' Power (watts)'] = 0.0*velo + + nr_rows = len(velo.values) + + df[' DriveLength (meters)'] = np.zeros(nr_rows) + df[' StrokeDistance (meters)'] = np.zeros(nr_rows) + df[' DriveTime (ms)'] = np.zeros(nr_rows) + df[' StrokeRecoveryTime (ms)'] = np.zeros(nr_rows) + df[' AverageDriveForce (lbs)'] = np.zeros(nr_rows) + df[' PeakDriveForce (lbs)'] = np.zeros(nr_rows) + df[' lapIdx'] = np.zeros(nr_rows) + + unixtime = cum_time+starttimeunix + unixtime[0] = starttimeunix + + df['TimeStamp (sec)'] = unixtime + + dt = np.diff(cum_time).mean() + wsize = round(5./dt) + + velo2 = ewmovingaverage(velo, wsize) + + df[' Stroke500mPace (sec/500m)'] = 500./velo2 + + df = df.fillna(0) + + df.sort_values(by='TimeStamp (sec)', ascending=True) + + + csvfilename = 'media/{code}_{importid}.csv'.format( + importid=importid, + code=uuid4().hex[:16] + ) + + res = df.to_csv(csvfilename+'.gz', index_label='index', + compression='gzip') + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': user.id, + 'file': csvfilename+'.gz', + 'title': '', + 'workouttype': workouttype, + 'boattype': '1x', + 'sporttracksid': importid, + 'title':title, + } + session = requests.session() + newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} + session.headers.update(newHeaders) + _ = session.post(UPLOAD_SERVICE_URL, json=uploadoptions) + + return 1 + @app.task def handle_sporttracks_sync(workoutid, url, headers, data, debug=False, **kwargs): @@ -2975,6 +3232,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 = """{ @@ -3056,6 +3316,7 @@ def handle_rp3_async_workout(userid, rp3token, rp3id, startdatetime, max_attempt 'boattype': '1x', 'rp3id': int(rp3id), 'startdatetime': startdatetime, + 'timezone': timezone, } session = requests.session() @@ -3086,7 +3347,6 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim data = alldata[int(nkid)] except KeyError: return 0 - params = { 'sessionIds': nkid, } diff --git a/rowers/templates/list_import.html b/rowers/templates/list_import.html new file mode 100644 index 00000000..fc4373a7 --- /dev/null +++ b/rowers/templates/list_import.html @@ -0,0 +1,95 @@ +{% extends "newbase.html" %} +{% load static %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block main %} +
This imports all workouts that have not been imported to rowsandall.com. + The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout. +
+No workouts found
+ {% endif %} +