def test_constant_representation(self): ''' ensures that gpx import/export is symetric ''' svcA, other = TestTools.create_mock_services() svcA.SupportsHR = svcA.SupportsCadence = svcA.SupportsTemp = True svcA.SupportsPower = svcA.SupportsCalories = False act = TestTools.create_random_activity(svcA, tz=True) mid = GPXIO.Dump(act) act2 = GPXIO.Parse(bytes(mid,"UTF-8")) act2.TZ = act.TZ # we need to fake this since local TZ isn't defined in GPX files, and TZ discovery will flail with random activities act2.AdjustTZ() act.Stats.Distance = act2.Stats.Distance = None # same here self.assertActivitiesEqual(act2, act)
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): 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): #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 UploadActivity(self, serviceRecord, activity): format = serviceRecord.GetConfiguration()["Format"] if format == "tcx": if "tcx" in activity.PrerenderedFormats: logger.debug("Using prerendered TCX") data = activity.PrerenderedFormats["tcx"] else: data = TCXIO.Dump(activity) else: if "gpx" in activity.PrerenderedFormats: logger.debug("Using prerendered GPX") data = activity.PrerenderedFormats["gpx"] else: data = GPXIO.Dump(activity) dbcl = self._getClient(serviceRecord) fname = self._format_file_name(serviceRecord.GetConfiguration()["Filename"], activity)[:250] + "." + format # DB has a max path component length of 255 chars, and we have to save for the file ext (4) and the leading slash (1) if not serviceRecord.Authorization["Full"]: fpath = "/" + fname else: fpath = serviceRecord.Config["SyncRoot"] + "/" + fname try: metadata = dbcl.files_upload(data.encode("UTF-8"), fpath, mode=dropbox.files.WriteMode.overwrite) except dropbox.exceptions.DropboxException as e: self._raiseDbException(e) # Fake this in so we don't immediately redownload the activity next time 'round cache = cachedb.dropbox_cache.find_one({"ExternalID": serviceRecord.ExternalID}) cache["Activities"][self._hash_path("/" + fname)] = {"Rev": metadata.rev, "UID": activity.UID, "StartTime": activity.StartTime.strftime("%H:%M:%S %d %m %Y %z"), "EndTime": activity.EndTime.strftime("%H:%M:%S %d %m %Y %z")} cachedb.dropbox_cache.update({"ExternalID": serviceRecord.ExternalID}, cache) # not upsert, hope the record exists at this time... return fpath
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 UploadActivity(self, serviceRecord, activity): """ POST a Multipart-Encoded File URL: https://app.velohero.com/upload/file Parameters: user = username pass = password view = json file = multipart-encodes file (fit, tcx, pwx, gpx, srm, hrm...) Maximum file size per file is 16 MB. """ has_location = has_distance = has_speed = False for lap in activity.Laps: for wp in lap.Waypoints: if wp.Location and wp.Location.Latitude and wp.Location.Longitude: has_location = True if wp.Distance: has_distance = True if wp.Speed: has_speed = True if has_location and has_distance and has_speed: format = "fit" data = FITIO.Dump(activity) elif has_location and has_distance: format = "tcx" data = TCXIO.Dump(activity) elif has_location: format = "gpx" data = GPXIO.Dump(activity) else: format = "fit" data = FITIO.Dump(activity) # Upload files = {"file": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + "." + format, data)} params = self._add_auth_params({"view":"json"}, record=serviceRecord) res = requests.post(self._urlRoot + "/upload/file", headers=self._obligatory_headers, files=files, params=params) if res.status_code != 200: if res.status_code == 403: raise APIException("Invalid login", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to upload activity") res.raise_for_status() try: res = res.json() except ValueError: raise APIException("Could not decode activity list") if "error" in res: raise APIException(res["error"]) # Set date, start time, comment and sport if "id" in res: workoutId = res["id"] params = self._add_auth_params({ "workout_date" : activity.StartTime.strftime("%Y-%m-%d"), "workout_start_time" : activity.StartTime.strftime("%H:%M:%S"), "workout_comment" : activity.Notes, "sport_id" : self._activityMappings[activity.Type], "workout_hide": "yes" if activity.Private else "no" }, record=serviceRecord) res = requests.get(self._urlRoot + "/workouts/change/{}".format(workoutId), headers=self._obligatory_headers, params=params) if res.status_code != 200: if res.status_code == 403: raise APIException("No authorization to change activity with workout ID: {}".format(workoutId), block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to change activity with workout ID: {}".format(workoutId)) return workoutId