Exemple #1
0
    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
Exemple #2
0
    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")
Exemple #3
0
    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
Exemple #4
0
    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
Exemple #5
0
 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
Exemple #6
0
    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
Exemple #7
0
    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
Exemple #8
0
    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
Exemple #9
0
    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
Exemple #10
0
    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"]
Exemple #11
0
    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"]
Exemple #12
0
    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
Exemple #13
0
    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)
Exemple #14
0
    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)
Exemple #15
0
    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
Exemple #16
0
    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
Exemple #17
0
    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
Exemple #18
0
    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
Exemple #19
0
    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
Exemple #20
0
    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
Exemple #21
0
    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
Exemple #22
0
    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
Exemple #23
0
    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
Exemple #24
0
    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")
Exemple #25
0
    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
Exemple #26
0
    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
Exemple #27
0
    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
Exemple #28
0
    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)