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 UploadActivity(self, serviceRecord, activity): # https://ridewithgps.com/trips.json tcx_file = TCXIO.Dump(activity) files = { "data_file": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".tcx", tcx_file) } params = {} params['trip[name]'] = activity.Name params[ 'trip[visibility]'] = 1 if activity.Private else 0 # Yes, this logic seems backwards but it's how it works res = requests.post("https://ridewithgps.com/trips.json", files=files, params=self._add_auth_params(params, record=serviceRecord)) if res.status_code % 100 == 4: raise APIException("Invalid login", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) res.raise_for_status() res = res.json() if res["success"] != 1: raise APIException("Unable to upload 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 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 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 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 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 UploadActivity(self, serviceRecord, activity): tcx_data = None # If some service provides ready-to-use tcx data why not to use it? if "tcx" in activity.PrerenderedFormats: tcx_data = activity.PrerenderedFormats["tcx"] # Set aerobia-understandable sport name tcx_data = re.sub(r'(Sport=\")[\w\s]+(\">)', r'\1{}\2'.format(self._activityMappings[activity.Type]), tcx_data) if tcx_data else None if not tcx_data: tcx_data = TCXIO.Dump(activity, self._activityMappings[activity.Type]) # Aerobia support activity names not longer than 60 characters activity_name = activity.Name[:60] if activity.Name else None data = {"name": activity_name, "description": activity.Notes} files = {"file": ("tap-sync-{}-{}.tcx".format(os.getpid(), activity.UID), tcx_data)} res = self._safe_call(serviceRecord, "post", self._uploadsUrl, data, files) res_obj = res.json() uploaded_id = res_obj["workouts"][0]["id"] if "error" in res_obj: raise APIException(res_obj["error"], user_exception=UserException(UserExceptionType.UploadError)) extra_data = {} if activity_name: extra_data.update({"workout[name]": activity_name}) self._put_default_inventory(activity, serviceRecord, extra_data) # Post extra data to newly uploaded activity if extra_data: self._patch_activity(serviceRecord, extra_data, uploaded_id) # return just uploaded activity id return uploaded_id
def UploadActivity(self, serviceRecord, activity): session = self._get_session(serviceRecord) tcx_data = None # If some service provides ready-to-use tcx data why not to use it? # TODO: please use this code when activity will have SourceFile property #if activity.SourceFile: # tcx_data = activity.SourceFile.getContent(ActivityFileType.TCX) # # Set aerobia-understandable sport name # tcx_data = re.sub(r'(<Sport=\")\w+(\">)', r'\1{}\2'.format(self._activityMappings[activity.Type]), tcx_data) if tcx_data else None if not tcx_data: # We pass an explicit activityType as the TCX schema doesn't define all the types this service supports. tcx_data = TCXIO.Dump( activity, activityType=self._activityMappings[activity.Type]) data = {"name": activity.Name, "description": activity.Notes} files = { "file": ("tap-sync-{}-{}.tcx".format(os.getpid(), activity.UID), tcx_data) } res = session.post(self._uploadsUrl, data=self._with_auth(serviceRecord, data), files=files) res_obj = res.json() if "error" in res_obj: raise APIException(res_obj["error"], user_exception=UserException( UserExceptionType.UploadError)) # return just uploaded activity id return res_obj["workouts"][0]["id"]
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 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 UploadActivity(self, serviceRecord, activity): session = self._get_session(serviceRecord) tcx_data = None # If some service provides ready-to-use tcx data why not to use it? if activity.SourceFile: tcx_data = activity.SourceFile.getContent(ActivityFileType.TCX) # Set aerobia-understandable sport name tcx_data = re.sub( r'(<Sport=\")\w+(\">)', r'\1{}\2'.format( self._activityMappings[activity.Type]), tcx_data) if tcx_data else None if not tcx_data: tcx_data = TCXIO.Dump(activity, self._activityMappings[activity.Type]) data = {"name": activity.Name, "description": activity.Notes} files = { "file": ("tap-sync-{}-{}.tcx".format(os.getpid(), activity.UID), tcx_data) } res = session.post(self._uploadsUrl, data=self._with_auth(serviceRecord, data), files=files) res_obj = res.json() uploaded_id = res_obj["workouts"][0]["id"] if "error" in res_obj: raise APIException(res_obj["error"], user_exception=UserException( UserExceptionType.UploadError)) extra_data = {} if activity.Name is not None: extra_data.update({"workout[name]": activity.Name}) self._put_default_inventory(activity, serviceRecord, extra_data) # Post extra data to newly uploaded activity if extra_data: self._patch_activity(serviceRecord, extra_data, uploaded_id) # return just uploaded activity id return uploaded_id
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
def UploadActivity(self, serviceRecord, activity): #/proxy/upload-service-1.1/json/upload/.tcx activity.EnsureTZ() tcx_file = TCXIO.Dump(activity) files = {"data": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".tcx", tcx_file)} cookies = self._get_cookies(record=serviceRecord) res = requests.post("http://connect.garmin.com/proxy/upload-service-1.1/json/upload/.tcx", files=files, cookies=cookies) res = res.json()["detailedImportResult"] if len(res["successes"]) != 1: raise APIException("Unable to upload activity") actid = res["successes"][0]["internalId"] if activity.Type not in [ActivityType.Running, ActivityType.Cycling, ActivityType.Other]: # Set the legit activity type - whatever it is, it's not supported by the TCX schema acttype = [k for k, v in self._reverseActivityMappings.items() if v == activity.Type] if len(acttype) == 0: raise APIWarning("GarminConnect does not support activity type " + activity.Type) else: acttype = acttype[0] res = requests.post("http://connect.garmin.com/proxy/activity-service-1.2/json/type/" + str(actid), data={"value": acttype}, cookies=cookies) res = res.json() if "activityType" not in res or res["activityType"]["key"] != acttype: raise APIWarning("Unable to set activity type")
def UploadActivity(self, serviceRecord, activity): # Upload the workout as a .TCX file uploaddata = TCXIO.Dump(activity) headers = self._apiHeaders(serviceRecord.Authorization) headers['Content-Type'] = 'application/xml' resp = requests.post(TRAINASONE_SERVER_URL + "/api/sync/activity/tcx", data=uploaddata, headers=headers) if resp.status_code != 200: raise APIException("Error uploading activity - " + str(resp.status_code), block=False) responseJson = resp.json() if not responseJson["id"]: raise APIException("Error uploading activity - " + resp.Message, block=False) activityId = responseJson["id"] return activityId
def UploadActivity(self, serviceRecord, activity): tcx_data = None # Patch tcx with notes if activity.Type != ActivityType.Report: if not activity.NotesExt and activity.Notes: tcx_data = TCXIO.Dump(activity) elif "tcx" in activity.PrerenderedFormats: tcx_data = activity.PrerenderedFormats["tcx"] else: tcx_data = TCXIO.Dump(activity) folder_base = os.path.join(USER_DATA_FILES, serviceRecord.ExternalID) # store reports in the separate folder if activity.Type == ActivityType.Report: folder_base = os.path.join(folder_base, "Posts") day_name_chunk = activity.StartTime.strftime("%Y-%m-%d") filename_base = "{}_{}".format(day_name_chunk, activity.Type) if activity.Name: if activity.Type == ActivityType.Report: filename_base = "{}_{}".format(day_name_chunk, activity.Name) else: filename_base = "{}_{}_{}".format(day_name_chunk, activity.Type, activity.Name) filename_base = django.utils.text.get_valid_filename(filename_base) name_base = os.path.join(folder_base, filename_base) if tcx_data: ext = ".tcx" file_exists = 1 while os.path.exists(name_base + ext): ext = "_{}.tcx".format(file_exists) file_exists = file_exists + 1 tcx_file_name = name_base + ext with open(tcx_file_name.decode("utf8"), 'w', encoding="utf-8") as file: file.write(tcx_data) if activity.NotesExt or len(activity.PhotoUrls): # Create subfolder only when need to save multiple files if tcx_data or len(activity.PhotoUrls): ext = "" folders_exists = 1 while os.path.exists(name_base + ext): ext = "_{}".format(folders_exists) folders_exists = folders_exists + 1 folder_base = name_base + ext os.mkdir(folder_base.decode("utf8")) for url_data in activity.PhotoUrls: img_file_name = "{}.jpg".format(url_data["id"]) img_file = os.path.join(folder_base, img_file_name) if activity.NotesExt: activity.NotesExt = activity.NotesExt.replace(url_data["url"], os.path.join(".", img_file_name)) self._download_image(url_data["url"], img_file) if activity.NotesExt: report_file_name = "{}.html".format(filename_base) note_file = os.path.join(folder_base, report_file_name) with open(note_file.decode("utf8"), 'w', encoding="utf-8") as file: file.write(activity.NotesExt) return serviceRecord.ExternalID + activity.UID
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
def UploadActivity(self, serviceRecord, activity): logger.info("Activity tz " + str(activity.TZ) + " dt tz " + str(activity.StartTime.tzinfo) + " starttime " + str(activity.StartTime)) req = { "id": 0, "data_type": "tcx", "external_id": "tap-sync-" + str(os.getpid()) + "-" + activity.UID + "-" + activity.UploadedTo[0]["Connection"].Service.ID, "activity_name": activity.Name, "activity_type": self._activityTypeMappings[activity.Type], "private": activity.Private } if "tcx" in activity.PrerenderedFormats: logger.debug("Using prerendered TCX") tcxData = activity.PrerenderedFormats["tcx"] else: activity.EnsureTZ() tcxData = TCXIO.Dump(activity) # TODO: put the tcx back into PrerenderedFormats once there's more RAM to go around and there's a possibility of it actually being used. files = {"file": (req["external_id"] + ".tcx", tcxData)} response = requests.post("http://www.strava.com/api/v3/uploads", data=req, files=files, headers=self._apiHeaders(serviceRecord)) if response.status_code != 201: if response.status_code == 401: raise APIException("No authorization to upload activity " + activity.UID + " response " + response.text + " status " + str(response.status_code), block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to upload activity " + activity.UID + " response " + response.text + " status " + str(response.status_code)) upload_id = response.json()["id"] while not response.json()["activity_id"]: time.sleep(1) response = requests.get("http://www.strava.com/api/v3/uploads/%s" % upload_id, headers=self._apiHeaders(serviceRecord)) logger.debug( "Waiting for upload - status %s id %s" % (response.json()["status"], response.json()["activity_id"])) if response.json()["error"]: error = response.json()["error"] if "duplicate of activity" in error: logger.debug("Duplicate") return # I guess we're done here? raise APIException( "Strava failed while processing activity - last status %s" % response.text)