def _getActivity(self, serviceRecord, dbcl, path): activityData = None try: f, metadata = dbcl.get_file_and_metadata(path) except rest.ErrorResponse as e: self._raiseDbException(e) if not activityData: activityData = f.read() try: if path.lower().endswith(".tcx"): act = TCXIO.Parse(activityData) else: act = GPXIO.Parse(activityData) except ValueError as e: raise APIExcludeActivity("Invalid GPX/TCX " + str(e), activityId=path, userException=UserException( UserExceptionType.Corrupt)) except lxml.etree.XMLSyntaxError as e: raise APIExcludeActivity("LXML parse error " + str(e), activityId=path, userException=UserException( UserExceptionType.Corrupt)) return act, metadata["rev"]
def DownloadActivity(self, serviceRecord, activity): #http://connect.garmin.com/proxy/activity-service-1.1/tcx/activity/#####?full=true activityID = activity.ServiceData["ActivityID"] cookies = self._get_cookies(record=serviceRecord) self._rate_limit() res = requests.get( "http://connect.garmin.com/proxy/activity-service-1.1/tcx/activity/" + str(activityID) + "?full=true", cookies=cookies) try: TCXIO.Parse(res.content, activity) except ValueError as e: raise APIExcludeActivity("TCX parse error " + str(e), userException=UserException( UserExceptionType.Corrupt)) if activity.ServiceData["RecalcHR"]: logger.debug("Recalculating HR") avgHR, maxHR = ActivityStatisticCalculator.CalculateAverageMaxHR( activity) activity.Stats.HR.coalesceWith( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=maxHR, avg=avgHR)) if len(activity.Laps) == 1: activity.Laps[0].Stats.update( activity.Stats ) # I trust Garmin Connect's stats more than whatever shows up in the TCX activity.Stats = activity.Laps[ 0].Stats # They must be identical to pass the verification if activity.Stats.Temperature.Min is not None or activity.Stats.Temperature.Max is not None or activity.Stats.Temperature.Average is not None: logger.debug("Retrieving additional temperature data") # TCX doesn't have temperature, for whatever reason... self._rate_limit() res = requests.get( "http://connect.garmin.com/proxy/activity-service-1.1/gpx/activity/" + str(activityID) + "?full=true", cookies=cookies) try: temp_act = GPXIO.Parse(res.content, suppress_validity_errors=True) except ValueError as e: pass else: logger.debug("Merging additional temperature data") full_waypoints = activity.GetFlatWaypoints() temp_waypoints = temp_act.GetFlatWaypoints() merge_idx = 0 for x in range(len(temp_waypoints)): while full_waypoints[merge_idx].Timestamp < temp_waypoints[ x].Timestamp and merge_idx < len( full_waypoints) - 1: merge_idx += 1 full_waypoints[merge_idx].Temp = temp_waypoints[x].Temp return activity
def DownloadActivity(self, serviceRecord, activity): session = self._get_session(serviceRecord) activity_id = activity.ServiceData["ActivityID"] tcx_data = session.get("{}export/workouts/{}/tcx".format( self._urlRoot, activity_id), data=self._with_auth(serviceRecord)) activity_ex = TCXIO.Parse(tcx_data.text.encode('utf-8'), activity) # Obtain more information about activity res = session.get(self._workoutUrlJson.format(id=activity_id), data=self._with_auth(serviceRecord)) activity_data = res.json() activity_ex.Name = activity_data["name"] # Notes comes as html. Hardly any other service will support this so needs to extract text data if "body" in activity_data["post"]: post_html = activity_data["post"]["body"] soup = BeautifulSoup(post_html) # Notes also contains styles, get rid of them for style in soup("style"): style.decompose() activity_ex.Notes = soup.getText() # Dirty hack to patch users inventory even if they use aerobia mobile app to record activities # Still need to sync with some service though. extra_data = {} self._put_default_inventory(activity, serviceRecord, extra_data) if extra_data: self._patch_activity(serviceRecord, extra_data, activity_id) return activity_ex
def DownloadActivity(self, serviceRecord, activity): deviceUploadFile = activity.ServiceData.get("DeviceUploadFile") # No additional data about this event is available. if not deviceUploadFile: return activity logger.info("Downloading device file %s" % deviceUploadFile) session = self._prepare_request(self._getUserToken(serviceRecord)) res = session.get(deviceUploadFile) if res.status_code == 200: try: contentType = self._mimeTypeMappings[res.headers["content-type"]] if not contentType: remoteUrl = urlparse(deviceUploadFile).path extension = os.path.splitext(remoteUrl)[1] contentType = self._fileExtensionMappings[extension] if contentType: if contentType == _DeviceFileTypes.FIT: # Oh no! Not supported! So close .... # FITIO.Parse(res.content, activity) return activity if contentType == _DeviceFileTypes.TCX: TCXIO.Parse(res.content, activity) if contentType == _DeviceFileTypes.GPX: GPXIO.Parse(res.content, activity) except ValueError as e: raise APIExcludeActivity("Parse error " + deviceUploadFile + " " + str(e), user_exception=UserException(UserExceptionType.Corrupt), permanent=True) return activity
def DownloadActivity(self, serviceRecord, activity): # First, download the summary stats and lap stats self._downloadActivitySummary(serviceRecord, activity) if len(activity.Laps) == 1: activity.Stats = activity.Laps[ 0].Stats # They must be identical to pass the verification if activity.Stationary: # Nothing else to download return activity # https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/### activityID = activity.ServiceData["ActivityID"] res = self._request_with_reauth( lambda session: session. get("https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/{}" .format(activityID)), serviceRecord) try: tcx_data = res.text activity = TCXIO.Parse(tcx_data.encode('utf-8'), activity) except ValueError: raise APIException("Activity data parse error for %s: %s" % (res.status_code, res.text)) return activity
def DownloadActivity(self, serviceRecord, activity): session = self._get_session(serviceRecord) url = "https://www.polarpersonaltrainer.com/user/calendar/" gpxUrl = "index.gpx" xmlUrl = "index.jxml" gpx_data = { ".action": "gpx", "items.0.item": activity.ExternalID, "items.0.itemType": "OptimizedExercise" } xml_data = { ".action": "export", "items.0.item": activity.ExternalID, "items.0.itemType": "OptimizedExercise", ".filename": "training.xml" } xmlResp = session.post(url + xmlUrl, data=xml_data) xmlText = xmlResp.text gpxResp = session.post(url + gpxUrl, data=gpx_data) if gpxResp.status_code == 401: logger.debug("Problem completing request. Unauthorized. Activity extId = {}".format(activity.ExternalID)) raise APIException("Unknown authorization problem during request", user_exception=UserException(UserExceptionType.DownloadError)) gpxText = gpxResp.text activity.GPS = not ("The items you are exporting contain no GPS data" in gpxText) tcxData = convert(xmlText, activity.StartTime, gpxText if activity.GPS else None) activity = TCXIO.Parse(tcxData, activity) return activity
def DownloadActivity(self, serviceRecord, activity): workout_id = activity.ServiceData["ID"] session = self._get_session(record=serviceRecord) res = session.get( "http://www.trainerroad.com/cycling/rides/download/%d" % workout_id) if res.status_code == 500: # Account is private (or their site is borked), log in the blegh way session = self._get_session(record=serviceRecord, cookieAuth=True) res = session.get( "http://www.trainerroad.com/cycling/rides/download/%d" % workout_id) activity.Private = True try: TCXIO.Parse(res.content, activity) except ValueError as e: raise APIExcludeActivity("TCX parse error " + str(e), user_exception=UserException( UserExceptionType.Corrupt)) return activity
def DownloadActivity(self, serviceRecord, activity): # NOTE tcx have to be gzipped but it actually doesn't # https://www.polar.com/accesslink-api/?python#get-tcx #tcx_data_raw = requests.get(activity_link + "/tcx", headers=self._api_headers(serviceRecord)) #tcx_data = gzip.GzipFile(fileobj=StringIO(tcx_data_raw)).read() tcx_url = serviceRecord.ServiceData[ "Transaction-uri"] + "/exercises/{}/tcx".format( activity.ServiceData["ActivityID"]) response = requests.get( tcx_url, headers=self._api_headers( serviceRecord, {"Accept": "application/vnd.garmin.tcx+xml"})) if response.status_code == 404: # Transaction was disbanded, all data linked to it will be returned in next transaction raise APIException("Transaction disbanded", user_exception=UserException( UserExceptionType.DownloadError)) try: tcx_data = response.text activity = TCXIO.Parse(tcx_data.encode('utf-8'), activity) activity.SourceFile = SourceFile(tcx_data, ActivityFileType.TCX) except lxml.etree.XMLSyntaxError: raise APIException( "Cannot recieve training tcx at url: {}".format(tcx_url), user_exception=UserException(UserExceptionType.DownloadError)) return activity
def test_constant_representation(self): ''' ensures that tcx import/export is symetric ''' script_dir = os.path.dirname(__file__) rel_path = "data/test1.tcx" source_file_path = os.path.join(script_dir, rel_path) with open(source_file_path, 'r') as testfile: data = testfile.read() act = TCXIO.Parse(data.encode('utf-8')) new_data = TCXIO.Dump(act) act2 = TCXIO.Parse(new_data.encode('utf-8')) rel_path = "data/output1.tcx" new_file_path = os.path.join(script_dir, rel_path) with open(new_file_path, "w") as new_file: new_file.write(new_data) self.assertActivitiesEqual(act2, act)
def test_garmin_tcx_export(self): ''' ensures that tcx exported from Garmin Connect can be correctly parsed ''' script_dir = os.path.dirname(__file__) rel_path = "data/garmin_parse_1.tcx" source_file_path = os.path.join(script_dir, rel_path) with open(source_file_path, 'r') as testfile: data = testfile.read() act = TCXIO.Parse(data.encode('utf-8')) act.PrerenderedFormats.clear() new_data = TCXIO.Dump(act) act2 = TCXIO.Parse(new_data.encode('utf-8')) rel_path = "data/output2.tcx" new_file_path = os.path.join(script_dir, rel_path) with open(new_file_path, "w") as new_file: new_file.write(new_data) self.assertActivitiesEqual(act2, act)
def DownloadActivity(self, svcRecord, activity): userID = svcRecord.ExternalID activity_id = activity.ServiceData["ActivityID"] logging.info("\t\t FITBIT LOADING : " + str(activity_id)) activity_tcx_uri = 'https://api.fitbit.com/1/user/' + userID + '/activities/' + str(activity_id) + '.tcx' resp = self._requestWithAuth(lambda session: session.get( activity_tcx_uri, headers={ 'Authorization': 'Bearer ' + svcRecord.Authorization.get('AccessToken') }), svcRecord) # check if request has error if resp.status_code != 204 and resp.status_code != 200: raise APIException("Unable to find Fitbit TCX activity") # Prepare tcxio params ns = copy.deepcopy(TCXIO.Namespaces) ns["tcx"] = ns[None] del ns[None] # Read tcx to know if this is a stationary activity or not try: root = etree.XML(resp.text.encode('utf-8')) except: root = etree.fromstring(resp.text.encode('utf-8')) xacts = root.find("tcx:Activities", namespaces=ns) if xacts is None: raise ValueError("No activities element in TCX") xact = xacts.find("tcx:Activity", namespaces=ns) if xact is None: raise ValueError("No activity element in TCX") # Define activity type from tcx if not activity.Type or activity.Type == ActivityType.Other: if xact.attrib["Sport"] == "Biking": activity.Type = ActivityType.Cycling elif xact.attrib["Sport"] == "Running": activity.Type = ActivityType.Running # Find all lap in tcx xlaps = xact.findall("tcx:Lap", namespaces=ns) if len(xlaps) > 0: activity = TCXIO.Parse(resp.text.encode('utf-8'), activity) else: # Define lap for activity lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] lap.Waypoints = [] activity.GPS = False activity.Stationary = len(lap.Waypoints) == 0 return activity
def DownloadActivity(self, serviceRecord, activity): #http://connect.garmin.com/proxy/activity-service-1.1/tcx/activity/#####?full=true activityID = [x["ActivityID"] for x in activity.UploadedTo if x["Connection"] == serviceRecord][0] cookies = self._get_cookies(record=serviceRecord) res = requests.get("http://connect.garmin.com/proxy/activity-service-1.1/tcx/activity/" + str(activityID) + "?full=true", cookies=cookies) try: TCXIO.Parse(res.content, activity) except ValueError as e: raise APIExcludeActivity("TCX parse error " + str(e)) return activity
def DownloadActivity(self, serviceRecord, activity): activity_id = activity.ServiceData["id"] # Switch URL to /api/sync/activity/fit/ once FITIO.Parse() available resp = requests.get(TRAINASONE_SERVER_URL + "/api/sync/activity/tcx/" + activity_id, headers=self._apiHeaders(serviceRecord.Authorization)) try: TCXIO.Parse(resp.content, activity) except ValueError as e: raise APIExcludeActivity("TCX parse error " + str(e), user_exception=UserException(UserExceptionType.Corrupt)) return activity
def DownloadActivity(self, serviceRecord, activity): if activity.Stationary: return activity # Nothing more to download - it doesn't serve these files for manually entered activites # https://ridewithgps.com/trips/??????.tcx activityID = activity.ServiceData["ActivityID"] res = requests.get("https://ridewithgps.com/trips/{}.tcx".format(activityID), params=self._add_auth_params({'sub_format': 'history'}, record=serviceRecord)) try: TCXIO.Parse(res.content, activity) except ValueError as e: raise APIExcludeActivity("TCX parse error " + str(e), user_exception=UserException(UserExceptionType.Corrupt)) return activity
def _getActivity(self, serviceRecord, dbcl, path, base_activity=None): try: metadata, file = dbcl.files_download(path) except dropbox.exceptions.DropboxException as e: self._raiseDbException(e) try: if path.lower().endswith(".tcx"): act = TCXIO.Parse(file.content, base_activity) else: act = GPXIO.Parse(file.content, base_activity) except ValueError as e: raise APIExcludeActivity("Invalid GPX/TCX " + str(e), activity_id=path, user_exception=UserException(UserExceptionType.Corrupt)) except lxml.etree.XMLSyntaxError as e: raise APIExcludeActivity("LXML parse error " + str(e), activity_id=path, user_exception=UserException(UserExceptionType.Corrupt)) return act, metadata.rev
def DownloadActivity(self, serviceRecord, activity): # https://ridewithgps.com/trips/??????.gpx activityID = [ x["ActivityID"] for x in activity.UploadedTo if x["Connection"] == serviceRecord ][0] res = requests.get( "https://ridewithgps.com/trips/{}.tcx".format(activityID), params=self._add_auth_params({'sub_format': 'history'}, record=serviceRecord)) try: TCXIO.Parse(res.content, activity) except ValueError as e: raise APIExcludeActivity("TCX parse error " + str(e), userException=UserException( UserExceptionType.Corrupt)) return activity
def DownloadActivity(self, serviceRecord, activity): activity_id = activity.ServiceData["ActivityID"] # reports already contains all data if activity.Type == ActivityType.Report: return activity tcx_data = self._safe_call(serviceRecord, "get", "{}export/workouts/{}/tcx".format(self._urlRoot, activity_id)) try: activity_ex = TCXIO.Parse(tcx_data.text.encode('utf-8'), activity) except: logger.debug("Unable to parse activity tcx: data corrupted") raise APIException("Unable to parse activity tcx: data corrupted") # Obtain more information about activity res = self._safe_call(serviceRecord, "get", self._workoutUrlJson.format(id=activity_id)) activity_data = res.json() activity_ex.Name = activity_data["name"] if "name" in activity_data else "" if "photos" in activity_data["post"]: for img_info in activity_data["post"]["photos"]: activity_ex.PhotoUrls.append({"id": img_info["id"], "url": img_info["original"]}) # Notes comes as html. Hardly any other service will support this so needs to extract text data if "body" in activity_data["post"]: post_html = activity_data["post"]["body"] soup = BeautifulSoup(post_html) # Notes also contains styles, get rid of them for style in soup("style"): style.decompose() activity_ex.Notes = soup.getText() # all notes with photos considered as reports if len(activity_ex.Notes) > self.REPORT_MIN_LIMIT or len(activity_ex.PhotoUrls): activity_ex.NotesExt = soup.prettify() # Dirty hack to patch users inventory even if they use aerobia mobile app to record activities # Still need to sync with some service though. # TODO should driven by setting #extra_data = {} #self._put_default_inventory(activity, serviceRecord, extra_data) #if extra_data: # self._patch_activity(serviceRecord, extra_data, activity_id) return activity_ex
def DownloadActivity(self, serviceRecord, activity): session = self._get_session(serviceRecord) activity_id = activity.ServiceData["ActivityID"] tcx_data = session.get("{}export/workouts/{}/tcx".format( self._urlRoot, activity_id), data=self._with_auth(serviceRecord)) activity_ex = TCXIO.Parse(tcx_data.text.encode('utf-8'), activity) # Obtain more information about activity res = session.get(self._workoutUrl.format(id=activity_id), data=self._with_auth(serviceRecord)) activity_data = res.json() activity_ex.Name = activity_data["name"] # Notes comes as html. Hardly any other service will support this so needs to extract text data if "body" in activity_data["post"]: post_html = activity_data["post"]["body"] soup = BeautifulSoup(post_html) # Notes also contains styles, get rid of them for style in soup("style"): style.decompose() activity_ex.Notes = soup.getText() return activity_ex