Private
Public Access
1
0

first attempt on c2

This commit is contained in:
Sander Roosendaal
2023-02-10 16:55:47 +01:00
parent d661f861c4
commit 2981c59a5d
19 changed files with 873 additions and 1129 deletions

View File

@@ -0,0 +1 @@
from .c2 import C2Integration

450
rowers/integrations/c2.py Normal file
View File

@@ -0,0 +1,450 @@
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
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__(self, *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_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).open(*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)
counter = 0
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', 'comment', 'new']
values = [i, d, ttot, s, r, s2, c, nnn]
ress = dict(zip(keys, values))
workouts.append(ress)
return workouts
# just as a quick test during development
u = User.objects.get(id=1)
c2_integration_1 = C2Integration(u)

View File

@@ -0,0 +1,250 @@
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
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[1]
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 createworkoutdata(w, *args, **kwargs) -> dict:
return {}
@abstractmethod
def workout_export(workout, *args, **kwargs) -> str:
pass
@abstractmethod
def get_workouts(*args, **kwargs) -> int:
pass
@abstractmethod
def get_workout(id) -> int:
pass
# need to unify workout list
@abstractmethod
def get_workout_list(*args, **kwargs) -> list:
pass
@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 = oauth_data['authorizaton_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
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):
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, oauth['refreshtokenname'])
if not refreshtoken:
refreshtoken = getattr(self.rower, oauth['tokenname'])
access_token, expires_in, refresh_token = self.do_refresh_token()
expirydatetime = timezone.now()+timedelta(seconds=expires_in)
setattr(self.rower, tokenname, access_token)
if expirydatename is not None:
setattr(self.rower, expirydatename, expirydatetime)
if refreshtokenname is not None:
setattr(self.rower, refreshtokenname, refresh_token)
self.rower.save()
return access_token