Ejemplo n.º 1
0
 def RevokeAuthorization(self, serviceRecord):
     resp = requests.post("https://runkeeper.com/apps/de-authorize", data={"access_token": serviceRecord.Authorization["Token"]})
     if resp.status_code != 204 and resp.status_code != 200:
         raise APIException("Unable to deauthorize RK auth token, status " + str(resp.status_code) + " resp " + resp.text)
     pass
Ejemplo n.º 2
0
    def UploadActivity(self, serviceRecord, activity):
        session = self._oauthSession(serviceRecord)
        device_id = self._deviceId(serviceRecord)
        if not serviceRecord.GetConfiguration()["DeviceRegistered"]:
            device_info = {
                "name": "tapiriik",
                "vendor": "tapiriik",
                "model": "tapiriik",
                "os": "tapiriik",
                "os_version": "1",
                "app_variant": "tapiriik",
                "app_version": "1"
            }
            device_add_resp = session.post(
                "https://api.endomondo.com/api/1/device/%s" % device_id,
                data=json.dumps(device_info))
            if device_add_resp.status_code != 200:
                self._rateLimitBailout(device_add_resp)
                raise APIException(
                    "Could not add device %s %s" %
                    (device_add_resp.status_code, device_add_resp.text))
            serviceRecord.SetConfiguration({"DeviceRegistered": True})

        activity_id = "tap-" + activity.UID + "-" + str(os.getpid())

        sport = self._getSport(activity)

        upload_data = {
            "device_id": device_id,
            "sport": sport,
            "start_time": self._formatDate(activity.StartTime),
            "end_time": self._formatDate(activity.EndTime),
            "points": []
        }

        if activity.Name:
            upload_data["title"] = activity.Name

        if activity.Notes:
            upload_data["notes"] = activity.Notes

        if activity.Stats.Distance.Value is not None:
            upload_data["distance_total"] = activity.Stats.Distance.asUnits(
                ActivityStatisticUnit.Kilometers).Value

        if activity.Stats.TimerTime.Value is not None:
            upload_data["duration_total"] = activity.Stats.TimerTime.asUnits(
                ActivityStatisticUnit.Seconds).Value
        elif activity.Stats.MovingTime.Value is not None:
            upload_data["duration_total"] = activity.Stats.MovingTime.asUnits(
                ActivityStatisticUnit.Seconds).Value
        else:
            upload_data["duration_total"] = (
                activity.EndTime - activity.StartTime).total_seconds()

        if activity.Stats.Energy.Value is not None:
            upload_data["calories_total"] = activity.Stats.Energy.asUnits(
                ActivityStatisticUnit.Kilocalories).Value

        elev_stats = activity.Stats.Elevation.asUnits(
            ActivityStatisticUnit.Meters)
        if elev_stats.Max is not None:
            upload_data["altitude_max"] = elev_stats.Max
        if elev_stats.Min is not None:
            upload_data["altitude_min"] = elev_stats.Min
        if elev_stats.Gain is not None:
            upload_data["total_ascent"] = elev_stats.Gain
        if elev_stats.Loss is not None:
            upload_data["total_descent"] = elev_stats.Loss

        speed_stats = activity.Stats.Speed.asUnits(
            ActivityStatisticUnit.KilometersPerHour)
        if speed_stats.Max is not None:
            upload_data["speed_max"] = speed_stats.Max

        hr_stats = activity.Stats.HR.asUnits(
            ActivityStatisticUnit.BeatsPerMinute)
        if hr_stats.Average is not None:
            upload_data["heart_rate_avg"] = hr_stats.Average
        if hr_stats.Max is not None:
            upload_data["heart_rate_max"] = hr_stats.Max

        if activity.Stats.Cadence.Average is not None:
            upload_data["cadence_avg"] = activity.Stats.Cadence.asUnits(
                ActivityStatisticUnit.RevolutionsPerMinute).Average
        elif activity.Stats.RunCadence.Average is not None:
            upload_data["cadence_avg"] = activity.Stats.RunCadence.asUnits(
                ActivityStatisticUnit.StepsPerMinute).Average

        if activity.Stats.Cadence.Max is not None:
            upload_data["cadence_max"] = activity.Stats.Cadence.asUnits(
                ActivityStatisticUnit.RevolutionsPerMinute).Max
        elif activity.Stats.RunCadence.Max is not None:
            upload_data["cadence_max"] = activity.Stats.RunCadence.asUnits(
                ActivityStatisticUnit.StepsPerMinute).Max

        if activity.Stats.Power.Average is not None:
            upload_data["power_avg"] = activity.Stats.Power.asUnits(
                ActivityStatisticUnit.Watts).Average

        if activity.Stats.Power.Max is not None:
            upload_data["power_max"] = activity.Stats.Power.asUnits(
                ActivityStatisticUnit.Watts).Max

        for wp in activity.GetFlatWaypoints():
            pt = {
                "time": self._formatDate(wp.Timestamp),
            }
            if wp.Location:
                if wp.Location.Latitude is not None and wp.Location.Longitude is not None:
                    pt["lat"] = wp.Location.Latitude
                    pt["lng"] = wp.Location.Longitude
                if wp.Location.Altitude is not None:
                    pt["alt"] = wp.Location.Altitude
            if wp.HR is not None:
                pt["hr"] = round(wp.HR)
            if wp.Cadence is not None:
                pt["cad"] = round(wp.Cadence)
            elif wp.RunCadence is not None:
                pt["cad"] = round(wp.RunCadence)

            if wp.Power is not None:
                pt["pow"] = round(wp.Power)

            if wp.Type == WaypointType.Pause:
                pt["inst"] = "pause"
            elif wp.Type == WaypointType.Resume:
                pt["inst"] = "resume"
            upload_data["points"].append(pt)

        if len(upload_data["points"]):
            upload_data["points"][0]["inst"] = "start"
            upload_data["points"][-1]["inst"] = "stop"

        upload_resp = session.post(
            "https://api.endomondo.com/api/1/workouts/%s" % activity_id,
            data=json.dumps(upload_data))
        if upload_resp.status_code != 200:
            self._rateLimitBailout(upload_resp)
            raise APIException("Could not upload activity %s %s" %
                               (upload_resp.status_code, upload_resp.text))

        return upload_resp.json()["id"]
Ejemplo n.º 3
0
    def DownloadActivityList(self, svcRecord, exhaustive=False):
        activities = []
        exclusions = []

        now = datetime.now()
        prev = now - timedelta(6 * 365 / 12)

        period = []

        aperiod = "%s%02d-%s%02d" % (prev.year, prev.month, now.year,
                                     now.month)
        period.append(aperiod)

        if exhaustive:
            for _ in range(20):
                now = prev
                prev = now - timedelta(6 * 365 / 12)
                aperiod = "%s%02d-%s%02d" % (prev.year, prev.month, now.year,
                                             now.month)
                period.append(aperiod)

        for dateInterval in period:
            headers = self._getAuthHeaders(svcRecord)
            resp = requests.get(self.ApiEndpoint + "/users/" +
                                str(svcRecord.ExternalID) +
                                "/activities.xml?date=" + dateInterval,
                                headers=headers)
            if resp.status_code == 400:
                logger.info(resp.content)
                raise APIException(
                    "No authorization to retrieve activity list",
                    block=True,
                    user_exception=UserException(
                        UserExceptionType.Authorization,
                        intervention_required=True))
            if resp.status_code == 401:
                logger.info(resp.content)
                raise APIException(
                    "No authorization to retrieve activity list",
                    block=True,
                    user_exception=UserException(
                        UserExceptionType.Authorization,
                        intervention_required=True))
            if resp.status_code == 403:
                logger.info(resp.content)
                raise APIException(
                    "No authorization to retrieve activity list",
                    block=True,
                    user_exception=UserException(
                        UserExceptionType.Authorization,
                        intervention_required=True))

            root = xml.fromstring(resp.content)

            logger.info("\t\t nb activity : " +
                        str(len(root.findall('.//ID'))))

            for ride in root.iter('ACTIVITY'):

                activity = UploadedActivity()
                activity.TZ = pytz.timezone("UTC")

                startdate = ride.find('.//STARTDATE').text + ride.find(
                    './/TIMEZONE').text
                datebase = parse(startdate)

                activity.StartTime = datebase  #pytz.utc.localize(datebase)

                activity.ServiceData = {
                    "ActivityID": ride.find('ID').text,
                    "Manual": ride.find('MANUAL').text
                }

                logger.info("\t\t DecathlonCoach Activity ID : " +
                            ride.find('ID').text)

                if ride.find('SPORTID'
                             ).text not in self._reverseActivityTypeMappings:
                    exclusions.append(
                        APIExcludeActivity("Unsupported activity type %s" %
                                           ride.find('SPORTID').text,
                                           activity_id=ride.find('ID').text,
                                           user_exception=UserException(
                                               UserExceptionType.Other)))
                    logger.info(
                        "\t\tDecathlonCoach Unknown activity, sport id " +
                        ride.find('SPORTID').text + " is not mapped")
                    continue

                activity.Type = self._reverseActivityTypeMappings[ride.find(
                    'SPORTID').text]

                for val in ride.iter('VALUE'):
                    if val.get('id') == self._unitMap["duration"]:
                        activity.EndTime = activity.StartTime + timedelta(
                            0, int(val.text))
                    if val.get('id') == self._unitMap["distance"]:
                        activity.Stats.Distance = ActivityStatistic(
                            ActivityStatisticUnit.Meters, value=int(val.text))
                    if val.get('id') == self._unitMap["kcal"]:
                        activity.Stats.Energy = ActivityStatistic(
                            ActivityStatisticUnit.Kilocalories,
                            value=int(val.text))
                    if val.get('id') == self._unitMap["speedaverage"]:
                        meterperhour = int(val.text)
                        meterpersecond = meterperhour / 3600
                        activity.Stats.Speed = ActivityStatistic(
                            ActivityStatisticUnit.MetersPerSecond,
                            avg=meterpersecond,
                            max=None)

                if ride.find('LIBELLE'
                             ).text == "" or ride.find('LIBELLE').text is None:
                    txtdate = startdate.split(' ')
                    activity.Name = "Sport DecathlonCoach " + txtdate[0]
                else:
                    activity.Name = ride.find('LIBELLE').text

                activity.Private = False
                activity.Stationary = ride.find('MANUAL').text
                activity.GPS = ride.find('ABOUT').find('TRACK').text
                activity.AdjustTZ()
                activity.CalculateUID()
                activities.append(activity)

        return activities, exclusions
Ejemplo n.º 4
0
    def UploadActivity(self, serviceRecord, activity):
        #/proxy/upload-service-1.1/json/upload/.fit
        fit_file = FITIO.Dump(activity)
        files = {
            "data":
            ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit",
             fit_file)
        }

        res = self._request_with_reauth(
            serviceRecord, lambda session: session.post(
                "https://connect.garmin.com/proxy/upload-service-1.1/json/upload/.fit",
                files=files))
        res = res.json()["detailedImportResult"]

        if len(res["successes"]) == 0:
            if len(res["failures"]) and len(
                    res["failures"][0]["messages"]) and res["failures"][0][
                        "messages"][0]["content"] == "Duplicate activity":
                logger.debug("Duplicate")
                return  # ...cool?
            raise APIException("Unable to upload activity %s" % res)
        if len(res["successes"]) > 1:
            raise APIException(
                "Uploaded succeeded, resulting in too many activities")
        actid = res["successes"][0]["internalId"]

        name = activity.Name  # Capture in logs
        notes = activity.Notes

        # Update activity metadata not included in the FIT file.
        metadata_object = {}
        if activity.Name and activity.Name.strip():
            metadata_object["activityName"] = activity.Name
        if activity.Notes and activity.Notes.strip():
            metadata_object["description"] = activity.Notes
        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 FIT 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]
            metadata_object["activityTypeDTO"] = {"typeKey": acttype}
        if activity.Private:
            metadata_object["accessControlRuleDTO"] = {"typeKey": "private"}

        if metadata_object:
            metadata_object["activityId"] = actid
            encoding_headers = {
                "Content-Type": "application/json; charset=UTF-8"
            }  # GC really, really needs this part, otherwise it throws obscure errors like "Invalid signature for signature method HMAC-SHA1"
            res = self._request_with_reauth(
                serviceRecord, lambda session: session.
                put("https://connect.garmin.com/proxy/activity-service/activity/"
                    + str(actid),
                    data=json.dumps(metadata_object),
                    headers=encoding_headers))
            if res.status_code != 204:
                raise APIWarning("Unable to set activity metadata - %d %s" %
                                 (res.status_code, res.text))

        return actid
Ejemplo n.º 5
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        oauthSession = self._oauthSession(serviceRecord)

        activities = []
        exclusions = []

        page_url = "https://api.endomondo.com/api/1/workouts"

        while True:
            resp = oauthSession.get(page_url)
            try:
                respList = resp.json()["data"]
            except ValueError:
                self._rateLimitBailout(resp)
                raise APIException("Error decoding activity list resp %s %s" %
                                   (resp.status_code, resp.text))
            for actInfo in respList:
                activity = UploadedActivity()
                activity.StartTime = self._parseDate(actInfo["start_time"])
                logger.debug("Activity s/t %s" % activity.StartTime)
                if "is_tracking" in actInfo and actInfo["is_tracking"]:
                    exclusions.append(
                        APIExcludeActivity(
                            "Not complete",
                            activity_id=actInfo["id"],
                            permanent=False,
                            user_exception=UserException(
                                UserExceptionType.LiveTracking)))
                    continue

                if "end_time" in actInfo:
                    activity.EndTime = self._parseDate(actInfo["end_time"])

                if actInfo["sport"] in self._activityMappings:
                    activity.Type = self._activityMappings[actInfo["sport"]]

                # "duration" is timer time
                if "duration_total" in actInfo:
                    activity.Stats.TimerTime = ActivityStatistic(
                        ActivityStatisticUnit.Seconds,
                        value=float(actInfo["duration_total"]))

                if "distance_total" in actInfo:
                    activity.Stats.Distance = ActivityStatistic(
                        ActivityStatisticUnit.Kilometers,
                        value=float(actInfo["distance_total"]))

                if "calories_total" in actInfo:
                    activity.Stats.Energy = ActivityStatistic(
                        ActivityStatisticUnit.Kilocalories,
                        value=float(actInfo["calories_total"]))

                activity.Stats.Elevation = ActivityStatistic(
                    ActivityStatisticUnit.Meters)

                if "altitude_max" in actInfo:
                    activity.Stats.Elevation.Max = float(
                        actInfo["altitude_max"])

                if "altitude_min" in actInfo:
                    activity.Stats.Elevation.Min = float(
                        actInfo["altitude_min"])

                if "total_ascent" in actInfo:
                    activity.Stats.Elevation.Gain = float(
                        actInfo["total_ascent"])

                if "total_descent" in actInfo:
                    activity.Stats.Elevation.Loss = float(
                        actInfo["total_descent"])

                activity.Stats.Speed = ActivityStatistic(
                    ActivityStatisticUnit.KilometersPerHour)
                if "speed_max" in actInfo:
                    activity.Stats.Speed.Max = float(actInfo["speed_max"])

                if "heart_rate_avg" in actInfo:
                    activity.Stats.HR = ActivityStatistic(
                        ActivityStatisticUnit.BeatsPerMinute,
                        avg=float(actInfo["heart_rate_avg"]))

                if "heart_rate_max" in actInfo:
                    activity.Stats.HR.update(
                        ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute,
                                          max=float(
                                              actInfo["heart_rate_max"])))

                if "cadence_avg" in actInfo:
                    activity.Stats.Cadence = ActivityStatistic(
                        ActivityStatisticUnit.RevolutionsPerMinute,
                        avg=int(actInfo["cadence_avg"]))

                if "cadence_max" in actInfo:
                    activity.Stats.Cadence.update(
                        ActivityStatistic(
                            ActivityStatisticUnit.RevolutionsPerMinute,
                            max=int(actInfo["cadence_max"])))

                if "power_avg" in actInfo:
                    activity.Stats.Power = ActivityStatistic(
                        ActivityStatisticUnit.Watts,
                        avg=int(actInfo["power_avg"]))

                if "power_max" in actInfo:
                    activity.Stats.Power.update(
                        ActivityStatistic(ActivityStatisticUnit.Watts,
                                          max=int(actInfo["power_max"])))

                if "title" in actInfo:
                    activity.Name = actInfo["title"]

                activity.ServiceData = {
                    "WorkoutID": int(actInfo["id"]),
                    "Sport": actInfo["sport"]
                }

                activity.CalculateUID()
                activities.append(activity)

            paging = resp.json()["paging"]
            if "next" not in paging or not paging["next"] or not exhaustive:
                break
            else:
                page_url = paging["next"]

        return activities, exclusions
Ejemplo n.º 6
0
    def _get_session(self,
                     record=None,
                     email=None,
                     password=None,
                     skip_cache=False):
        from tapiriik.auth.credential_storage import CredentialStore
        cached = self._sessionCache.Get(record.ExternalID if record else email)
        if cached and not skip_cache:
            logger.debug("Using cached credential")
            return cached
        if record:
            #  longing for C style overloads...
            password = CredentialStore.Decrypt(
                record.ExtendedAuthorization["Password"])
            email = CredentialStore.Decrypt(
                record.ExtendedAuthorization["Email"])

        session = requests.Session()

        # JSIG CAS, cool I guess.
        # Not quite OAuth though, so I'll continue to collect raw credentials.
        # Commented stuff left in case this ever breaks because of missing parameters...
        data = {
            "username": email,
            "password": password,
            "_eventId": "submit",
            "embed": "true",
            # "displayNameRequired": "false"
        }
        params = {
            "service": "https://connect.garmin.com/post-auth/login",
            # "redirectAfterAccountLoginUrl": "http://connect.garmin.com/post-auth/login",
            # "redirectAfterAccountCreationUrl": "http://connect.garmin.com/post-auth/login",
            # "webhost": "olaxpw-connect00.garmin.com",
            "clientId": "GarminConnect",
            # "gauthHost": "https://sso.garmin.com/sso",
            # "rememberMeShown": "true",
            # "rememberMeChecked": "false",
            "consumeServiceTicket": "false",
            # "id": "gauth-widget",
            # "embedWidget": "false",
            # "cssUrl": "https://static.garmincdn.com/com.garmin.connect/ui/src-css/gauth-custom.css",
            # "source": "http://connect.garmin.com/en-US/signin",
            # "createAccountShown": "true",
            # "openCreateAccount": "false",
            # "usernameShown": "true",
            # "displayNameShown": "false",
            # "initialFocus": "true",
            # "locale": "en"
        }
        # I may never understand what motivates people to mangle a perfectly good protocol like HTTP in the ways they do...
        preResp = session.get("https://sso.garmin.com/sso/login",
                              params=params)
        if preResp.status_code != 200:
            raise APIException("SSO prestart error %s %s" %
                               (preResp.status_code, preResp.text))
        data["lt"] = re.search("name=\"lt\"\s+value=\"([^\"]+)\"",
                               preResp.text).groups(1)[0]

        ssoResp = session.post("https://sso.garmin.com/sso/login",
                               params=params,
                               data=data,
                               allow_redirects=False)
        if ssoResp.status_code != 200 or "temporarily unavailable" in ssoResp.text:
            raise APIException("SSO error %s %s" %
                               (ssoResp.status_code, ssoResp.text))

        ticket_match = re.search("ticket=([^']+)'", ssoResp.text)
        if not ticket_match:
            raise APIException("Invalid login",
                               block=True,
                               user_exception=UserException(
                                   UserExceptionType.Authorization,
                                   intervention_required=True))
        ticket = ticket_match.groups(1)[0]

        # ...AND WE'RE NOT DONE YET!

        self._rate_limit()
        gcRedeemResp = session.get(
            "https://connect.garmin.com/post-auth/login",
            params={"ticket": ticket},
            allow_redirects=False)
        if gcRedeemResp.status_code != 302:
            raise APIException("GC redeem-start error %s %s" %
                               (gcRedeemResp.status_code, gcRedeemResp.text))

        # There are 6 redirects that need to be followed to get the correct cookie
        # ... :(
        expected_redirect_count = 6
        current_redirect_count = 1
        while True:
            self._rate_limit()
            gcRedeemResp = session.get(gcRedeemResp.headers["location"],
                                       allow_redirects=False)

            if current_redirect_count >= expected_redirect_count and gcRedeemResp.status_code != 200:
                raise APIException(
                    "GC redeem %d/%d error %s %s" %
                    (current_redirect_count, expected_redirect_count,
                     gcRedeemResp.status_code, gcRedeemResp.text))
            if gcRedeemResp.status_code == 200 or gcRedeemResp.status_code == 404:
                break
            current_redirect_count += 1
            if current_redirect_count > expected_redirect_count:
                break

        self._sessionCache.Set(record.ExternalID if record else email, session)

        session.headers.update(self._obligatory_headers)

        return session
Ejemplo n.º 7
0
    def _downloadActivitySummary(self, serviceRecord, activity):
        activityID = activity.ServiceData["ActivityID"]

        res = self._request_with_reauth(
            serviceRecord, lambda session: session.
            get("https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activity/"
                + str(activityID)))

        try:
            raw_data = res.json()
        except ValueError:
            raise APIException("Failure downloading activity summary %s:%s" %
                               (res.status_code, res.text))
        stat_map = {}

        def mapStat(gcKey, statKey, type):
            stat_map[gcKey] = {"key": statKey, "attr": type}

        def applyStats(gc_dict, stats_obj):
            for gc_key, stat in stat_map.items():
                if gc_key in gc_dict:
                    value = float(gc_dict[gc_key]["value"])
                    units = self._unitMap[gc_dict[gc_key]["uom"]]
                    if math.isinf(value):
                        continue  # GC returns the minimum speed as "-Infinity" instead of 0 some times :S
                    getattr(stats_obj, stat["key"]).update(
                        ActivityStatistic(units, **({
                            stat["attr"]: value
                        })))

        mapStat("SumMovingDuration", "MovingTime", "value")
        mapStat("SumDuration", "TimerTime", "value")
        mapStat("SumDistance", "Distance", "value")
        mapStat("MinSpeed", "Speed", "min")
        mapStat("MaxSpeed", "Speed", "max")
        mapStat("WeightedMeanSpeed", "Speed", "avg")
        mapStat("MinAirTemperature", "Temperature", "min")
        mapStat("MaxAirTemperature", "Temperature", "max")
        mapStat("WeightedMeanAirTemperature", "Temperature", "avg")
        mapStat("SumEnergy", "Energy", "value")
        mapStat("MaxHeartRate", "HR", "max")
        mapStat("WeightedMeanHeartRate", "HR", "avg")
        mapStat("MaxDoubleCadence", "RunCadence", "max")
        mapStat("WeightedMeanDoubleCadence", "RunCadence", "avg")
        mapStat("MaxBikeCadence", "Cadence", "max")
        mapStat("WeightedMeanBikeCadence", "Cadence", "avg")
        mapStat("MinPower", "Power", "min")
        mapStat("MaxPower", "Power", "max")
        mapStat("WeightedMeanPower", "Power", "avg")
        mapStat("MinElevation", "Elevation", "min")
        mapStat("MaxElevation", "Elevation", "max")
        mapStat("GainElevation", "Elevation", "gain")
        mapStat("LossElevation", "Elevation", "loss")

        applyStats(raw_data["activity"]["activitySummary"], activity.Stats)

        for lap_data in raw_data["activity"]["totalLaps"]["lapSummaryList"]:
            lap = Lap()
            if "BeginTimestamp" in lap_data:
                lap.StartTime = pytz.utc.localize(
                    datetime.utcfromtimestamp(
                        float(lap_data["BeginTimestamp"]["value"]) / 1000))
            if "EndTimestamp" in lap_data:
                lap.EndTime = pytz.utc.localize(
                    datetime.utcfromtimestamp(
                        float(lap_data["EndTimestamp"]["value"]) / 1000))

            elapsed_duration = None
            if "SumElapsedDuration" in lap_data:
                elapsed_duration = timedelta(seconds=round(
                    float(lap_data["SumElapsedDuration"]["value"])))
            elif "SumDuration" in lap_data:
                elapsed_duration = timedelta(
                    seconds=round(float(lap_data["SumDuration"]["value"])))

            if lap.StartTime and elapsed_duration:
                # Always recalculate end time based on duration, if we have the start time
                lap.EndTime = lap.StartTime + elapsed_duration
            if not lap.StartTime and lap.EndTime and elapsed_duration:
                # Sometimes calculate start time based on duration
                lap.StartTime = lap.EndTime - elapsed_duration

            if not lap.StartTime or not lap.EndTime:
                # Garmin Connect is weird.
                raise APIExcludeActivity(
                    "Activity lap has no BeginTimestamp or EndTimestamp",
                    user_exception=UserException(UserExceptionType.Corrupt))

            applyStats(lap_data, lap.Stats)
            activity.Laps.append(lap)

        # In Garmin Land, max can be smaller than min for this field :S
        if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max:
            activity.Stats.Power.Min = None
Ejemplo n.º 8
0
    def DownloadActivityList(self, svcRecord, exhaustive=False):
        activities = []
        exclusions = []
        before = earliestDate = None

        while True:
            if before is not None and before < 0:
                break # Caused by activities that "happened" before the epoch. We generally don't care about those activities...
            logger.debug("Req with before=" + str(before) + "/" + str(earliestDate))
            resp = requests.get("https://www.strava.com/api/v3/athletes/" + str(svcRecord.ExternalID) + "/activities", headers=self._apiHeaders(svcRecord), params={"before": before})
            if resp.status_code == 401:
                raise APIException("No authorization to retrieve activity list", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True))

            earliestDate = None

            try:
                reqdata = resp.json()
            except ValueError:
                raise APIException("Failed parsing strava list response %s - %s" % (resp.status_code, resp.text))

            if not len(reqdata):
                break  # No more activities to see

            for ride in reqdata:
                activity = UploadedActivity()
                activity.TZ = pytz.timezone(re.sub("^\([^\)]+\)\s*", "", ride["timezone"]))  # Comes back as "(GMT -13:37) The Stuff/We Want""
                activity.StartTime = pytz.utc.localize(datetime.strptime(ride["start_date"], "%Y-%m-%dT%H:%M:%SZ"))
                logger.debug("\tActivity s/t %s: %s" % (activity.StartTime, ride["name"]))
                if not earliestDate or activity.StartTime < earliestDate:
                    earliestDate = activity.StartTime
                    before = calendar.timegm(activity.StartTime.astimezone(pytz.utc).timetuple())

                activity.EndTime = activity.StartTime + timedelta(0, ride["elapsed_time"])
                activity.ServiceData = {"ActivityID": ride["id"], "Manual": ride["manual"]}

                if ride["type"] not in self._reverseActivityTypeMappings:
                    exclusions.append(APIExcludeActivity("Unsupported activity type %s" % ride["type"], activity_id=ride["id"], user_exception=UserException(UserExceptionType.Other)))
                    logger.debug("\t\tUnknown activity")
                    continue

                activity.Type = self._reverseActivityTypeMappings[ride["type"]]
                activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=ride["distance"])
                if "max_speed" in ride or "average_speed" in ride:
                    activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=ride["average_speed"] if "average_speed" in ride else None, max=ride["max_speed"] if "max_speed" in ride else None)
                activity.Stats.MovingTime = ActivityStatistic(ActivityStatisticUnit.Seconds, value=ride["moving_time"] if "moving_time" in ride and ride["moving_time"] > 0 else None)  # They don't let you manually enter this, and I think it returns 0 for those activities.
                # Strava doesn't handle "timer time" to the best of my knowledge - although they say they do look at the FIT total_timer_time field, so...?
                if "average_watts" in ride:
                    activity.Stats.Power = ActivityStatistic(ActivityStatisticUnit.Watts, avg=ride["average_watts"])
                if "average_heartrate" in ride:
                    activity.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=ride["average_heartrate"]))
                if "max_heartrate" in ride:
                    activity.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=ride["max_heartrate"]))
                if "average_cadence" in ride:
                    activity.Stats.Cadence.update(ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, avg=ride["average_cadence"]))
                if "average_temp" in ride:
                    activity.Stats.Temperature.update(ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, avg=ride["average_temp"]))
                if "calories" in ride:
                    activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=ride["calories"])
                activity.Name = ride["name"]
                activity.Private = ride["private"]
                activity.Stationary = ride["manual"]
                activity.GPS = ("start_latlng" in ride) and (ride["start_latlng"] is not None)
                activity.AdjustTZ()
                activity.CalculateUID()
                activities.append(activity)

            if not exhaustive or not earliestDate:
                break

        return activities, exclusions
Ejemplo n.º 9
0
    def UploadActivity(self, serviceRecord, activity):
        logger.info("Activity tz " + str(activity.TZ) + " dt tz " + str(activity.StartTime.tzinfo) + " starttime " + str(activity.StartTime))

        if self.LastUpload is not None:
            while (datetime.now() - self.LastUpload).total_seconds() < 5:
                time.sleep(1)
                logger.debug("Inter-upload cooldown")
        source_svc = None
        if hasattr(activity, "ServiceDataCollection"):
            source_svc = str(list(activity.ServiceDataCollection.keys())[0])

        upload_id = None
        if activity.CountTotalWaypoints():
            req = {
                    "data_type": "fit",
                    "activity_name": activity.Name,
                    "description": activity.Notes, # Paul Mach said so.
                    "activity_type": self._activityTypeMappings[activity.Type],
                    "private": 1 if activity.Private else 0}

            if "fit" in activity.PrerenderedFormats:
                logger.debug("Using prerendered FIT")
                fitData = activity.PrerenderedFormats["fit"]
            else:
                # TODO: put the fit back into PrerenderedFormats once there's more RAM to go around and there's a possibility of it actually being used.
                fitData = FITIO.Dump(activity, drop_pauses=True)
            files = {"file":("tap-sync-" + activity.UID + "-" + str(os.getpid()) + ("-" + source_svc if source_svc else "") + ".fit", fitData)}

            response = requests.post("https://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))
                if "duplicate of activity" in response.text:
                    logger.debug("Duplicate")
                    self.LastUpload = datetime.now()
                    return # Fine by me. The majority of these cases were caused by a dumb optimization that meant existing activities on services were never flagged as such if tapiriik didn't have to synchronize them elsewhere.
                raise APIException("Unable to upload activity " + activity.UID + " response " + response.text + " status " + str(response.status_code))

            upload_id = response.json()["id"]
            upload_poll_wait = 8 # The mode of processing times
            while not response.json()["activity_id"]:
                time.sleep(upload_poll_wait)
                response = requests.get("https://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:
                        self.LastUpload = datetime.now()
                        logger.debug("Duplicate")
                        return # I guess we're done here?
                    raise APIException("Strava failed while processing activity - last status %s" % response.text)
            upload_id = response.json()["activity_id"]
        else:
            localUploadTS = activity.StartTime.strftime("%Y-%m-%d %H:%M:%S")
            req = {
                    "name": activity.Name if activity.Name else activity.StartTime.strftime("%d/%m/%Y"), # This is required
                    "description": activity.Notes,
                    "type": self._activityTypeMappings[activity.Type],
                    "private": 1 if activity.Private else 0,
                    "start_date_local": localUploadTS,
                    "distance": activity.Stats.Distance.asUnits(ActivityStatisticUnit.Meters).Value,
                    "elapsed_time": round((activity.EndTime - activity.StartTime).total_seconds())
                }
            headers = self._apiHeaders(serviceRecord)
            response = requests.post("https://www.strava.com/api/v3/activities", data=req, headers=headers)
            # FFR this method returns the same dict as the activity listing, as REST services are wont to do.
            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 stationary activity " + activity.UID + " response " + response.text + " status " + str(response.status_code))
            upload_id = response.json()["id"]

        self.LastUpload = datetime.now()
        return upload_id
Ejemplo n.º 10
0
    def _get_session(self,
                     record=None,
                     email=None,
                     password=None,
                     skip_cache=False):
        from tapiriik.auth.credential_storage import CredentialStore
        cached = self._sessionCache.Get(record.ExternalID if record else email)
        if cached and not skip_cache:
            return cached
        if record:
            #  longing for C style overloads...
            password = CredentialStore.Decrypt(
                record.ExtendedAuthorization["Password"])
            email = CredentialStore.Decrypt(
                record.ExtendedAuthorization["Email"])

        session = requests.Session()
        self._rate_limit()
        mPreResp = session.get(self._urlRoot + "/api/tapiriikProfile",
                               allow_redirects=False)
        # New site gets this redirect, old one does not
        if mPreResp.status_code == 403:
            data = {
                "_username": email,
                "_password": password,
                "_remember_me": "true",
            }
            preResp = session.post(self._urlRoot + "/api/login", data=data)

            if preResp.status_code != 200:
                raise APIException("Login error %s %s" %
                                   (preResp.status_code, preResp.text))

            try:
                preResp = preResp.json()
            except ValueError:
                raise APIException("Parse error %s %s" %
                                   (preResp.status_code, preResp.text),
                                   block=True,
                                   user_exception=UserException(
                                       UserExceptionType.Authorization,
                                       intervention_required=True))

            if "success" not in preResp and "error" not in preResp:
                raise APIException("Login error",
                                   block=True,
                                   user_exception=UserException(
                                       UserExceptionType.Authorization,
                                       intervention_required=True))

            success = True
            error = ""

            if "success" in preResp:
                success = ["success"]

            if "error" in preResp:
                error = preResp["error"]

            if not success:
                logger.debug("Login error %s" % (error))
                raise APIException("Invalid login",
                                   block=True,
                                   user_exception=UserException(
                                       UserExceptionType.Authorization,
                                       intervention_required=True))

            # Double check

            self._rate_limit()
            mRedeemResp1 = session.get(self._urlRoot + "/api/tapiriikProfile",
                                       allow_redirects=False)
            if mRedeemResp1.status_code != 200:
                raise APIException(
                    "Motivato redeem error %s %s" %
                    (mRedeemResp1.status_code, mRedeemResp1.text))

        else:
            logger.debug("code %s" % mPreResp.status_code)
            raise APIException("Unknown Motivato prestart response %s %s" %
                               (mPreResp.status_code, mPreResp.text))

        self._sessionCache.Set(record.ExternalID if record else email, session)

        session.headers.update(self._obligatory_headers)

        return session
Ejemplo n.º 11
0
 def RevokeAuthorization(self, serviceRecord):
     resp = requests.post("https://www.strava.com/oauth/deauthorize", headers=self._apiHeaders(serviceRecord))
     if resp.status_code != 204 and resp.status_code != 200:
         raise APIException("Unable to deauthorize Strava auth token, status " + str(resp.status_code) + " resp " + resp.text)
     pass
Ejemplo n.º 12
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        logger.debug("Checking motivato premium state")
        self._applyPaymentState(serviceRecord)

        logger.debug("Motivato DownloadActivityList")
        session = self._get_session(record=serviceRecord)
        activities = []
        exclusions = []

        self._rate_limit()

        retried_auth = False
        #headers = {'X-App-With-Tracks': "true"}
        headers = {}
        res = session.post(self._urlRoot + "/api/workouts/sync",
                           headers=headers)

        if res.status_code == 403 and not retried_auth:
            retried_auth = True
            session = self._get_session(serviceRecord, skip_cache=True)

        try:
            respList = res.json()
        except ValueError:
            res_txt = res.text  # So it can capture in the log message
            raise APIException("Parse failure in Motivato list resp: %s" %
                               res.status_code)

        for actInfo in respList:
            if "duration" in actInfo:
                duration = self._durationToSeconds(actInfo["duration"])
            else:
                continue

            activity = UploadedActivity()
            if "time_start" in actInfo["metas"]:
                startTimeStr = actInfo["training_at"] + " " + actInfo["metas"][
                    "time_start"]
            else:
                startTimeStr = actInfo["training_at"] + " 00:00:00"

            activity.StartTime = self._parseDateTime(startTimeStr)
            activity.EndTime = self._parseDateTime(startTimeStr) + timedelta(
                seconds=duration)
            activity.Type = self._reverseActivityMappings[
                actInfo["discipline_id"]]
            activity.Stats.TimerTime = ActivityStatistic(
                ActivityStatisticUnit.Seconds, value=duration)
            if "distance" in actInfo:
                activity.Stats.Distance = ActivityStatistic(
                    ActivityStatisticUnit.Kilometers,
                    value=float(actInfo["distance"]))
            #activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.KilometersPerSecond, value=1.0/float(actInfo["metas"]["pace"]))

            activity.ServiceData = {"WorkoutID": int(actInfo["id"])}

            activity.CalculateUID()
            logger.debug("Generated UID %s" % activity.UID)
            activities.append(activity)

        return activities, exclusions
Ejemplo n.º 13
0
    def UploadActivity(self, serviceRecord, activity):
        logger.debug("Motivato UploadActivity")
        session = self._get_session(record=serviceRecord)

        dic = dict(training_at=activity.StartTime.strftime("%Y-%m-%d"),
                   distance=activity.Stats.Distance.asUnits(
                       ActivityStatisticUnit.Kilometers).Value,
                   duration="",
                   user_comment=activity.Notes,
                   updated_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                   created_at=activity.StartTime.strftime("%Y-%m-%d %H:%M:%S"),
                   discipline_id=self._activityMappings[activity.Type],
                   source_id=8,
                   metas=dict(
                       distance=activity.Stats.Distance.asUnits(
                           ActivityStatisticUnit.Kilometers).Value,
                       duration="",
                       time_start=activity.StartTime.strftime("%H:%M:%S")),
                   track={})

        if activity.Stats.TimerTime.Value is not None:
            secs = activity.Stats.TimerTime.asUnits(
                ActivityStatisticUnit.Seconds).Value
        elif activity.Stats.MovingTime.Value is not None:
            secs = activity.Stats.MovingTime.asUnits(
                ActivityStatisticUnit.Seconds).Value
        else:
            secs = (activity.EndTime - activity.StartTime).total_seconds()

        dic["metas"]["duration"] = str(timedelta(seconds=secs))
        dic["duration"] = str(timedelta(seconds=secs))

        pace = str(timedelta(seconds=secs / activity.Stats.Distance.Value))
        meta_hr_avg = activity.Stats.HR.Average
        meta_hr_max = activity.Stats.HR.Max

        if pace:
            dic["metas"]["pace"] = pace

        if meta_hr_avg:
            dic["metas"]["meta_hr_avg"] = meta_hr_avg

        if meta_hr_max:
            dic["metas"]["meta_hr_max"] = meta_hr_max

        if len(activity.Laps) > 0:
            dic["track"] = dict(name=activity.Name, mtime=secs, points=[])

            for tk in activity.Laps:
                for wpt in tk.Waypoints:
                    pt = dict(
                        lat=wpt.Location.Latitude,
                        lon=wpt.Location.Longitude,
                        ele=wpt.Location.Altitude,
                        bpm=wpt.HR,
                        moment=wpt.Timestamp.strftime('%Y-%m-%d %H:%M:%S'))

                    if wpt.Speed and wpt.Speed != None and wpt.Speed != 0:
                        pt["pace"] = (1000.0 / wpt.Speed)

                    dic["track"]["points"].append(pt)

        toSend = json.dumps(dic)

        try:
            res = session.post(self._urlRoot + "/api/workout", data=toSend)
        except APIWarning as e:
            raise APIException(str(e))

        if res.status_code != 201:
            raise APIException("Activity didn't upload: %s, %s" %
                               (res.status_code, res.text))

        try:
            retJson = res.json()
        except ValueError:
            raise APIException("Activity upload parse error for %s, %s" %
                               (res.status_code, res.text))

        return retJson["id"]
Ejemplo n.º 14
0
    def DownloadActivity(self, svcRecord, activity):
        if activity.ServiceData[
                "Manual"]:  # I should really add a param to DownloadActivity for this value as opposed to constantly doing this
            # We've got as much information as we're going to get - we need to copy it into a Lap though.
            activity.Laps = [
                Lap(startTime=activity.StartTime,
                    endTime=activity.EndTime,
                    stats=activity.Stats)
            ]
            return activity
        activityID = activity.ServiceData["ActivityID"]

        streamdata = self._requestWithAuth(
            lambda session: session.
            get("https://www.strava.com/api/v3/activities/" + str(activityID) +
                "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng,distance,velocity_smooth"
                ), svcRecord)
        if streamdata.status_code == 401:
            raise APIException("No authorization to download activity",
                               block=True,
                               user_exception=UserException(
                                   UserExceptionType.Authorization,
                                   intervention_required=True))

        try:
            streamdata = streamdata.json()
        except:
            raise APIException("Stream data returned is not JSON")

        if "message" in streamdata and streamdata[
                "message"] == "Record Not Found":
            raise APIException("Could not find activity")

        ridedata = {}
        for stream in streamdata:
            ridedata[stream["type"]] = stream["data"]

        lap = Lap(
            stats=activity.Stats,
            startTime=activity.StartTime,
            endTime=activity.EndTime
        )  # Strava doesn't support laps, but we need somewhere to put the waypoints.
        activity.Laps = [lap]
        lap.Waypoints = []

        hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0
        hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0
        hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0
        hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0)
        hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0
        hasDistance = "distance" in ridedata and len(ridedata["distance"]) > 0
        hasVelocity = "velocity_smooth" in ridedata and len(
            ridedata["velocity_smooth"]) > 0

        if "error" in ridedata:
            raise APIException("Strava error " + ridedata["error"])

        inPause = False

        waypointCt = len(ridedata["time"])
        for idx in range(0, waypointCt - 1):

            waypoint = Waypoint(activity.StartTime +
                                timedelta(0, ridedata["time"][idx]))
            if "latlng" in ridedata:
                latlng = ridedata["latlng"][idx]
                waypoint.Location = Location(latlng[0], latlng[1], None)
                if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0:
                    waypoint.Location.Longitude = None
                    waypoint.Location.Latitude = None

            if hasAltitude:
                if not waypoint.Location:
                    waypoint.Location = Location(None, None, None)
                waypoint.Location.Altitude = float(ridedata["altitude"][idx])

            # When pausing, Strava sends this format:
            # idx = 100 ; time = 1000; moving = true
            # idx = 101 ; time = 1001; moving = true  => convert to Pause
            # idx = 102 ; time = 2001; moving = false => convert to Resume: (2001-1001) seconds pause
            # idx = 103 ; time = 2002; moving = true

            if idx == 0:
                waypoint.Type = WaypointType.Start
            elif idx == waypointCt - 2:
                waypoint.Type = WaypointType.End
            elif idx < waypointCt - 2 and ridedata["moving"][idx +
                                                             1] and inPause:
                waypoint.Type = WaypointType.Resume
                inPause = False
            elif idx < waypointCt - 2 and not ridedata["moving"][
                    idx + 1] and not inPause:
                waypoint.Type = WaypointType.Pause
                inPause = True

            if hasHR:
                waypoint.HR = ridedata["heartrate"][idx]
            if hasCadence:
                waypoint.Cadence = ridedata["cadence"][idx]
            if hasTemp:
                waypoint.Temp = ridedata["temp"][idx]
            if hasPower:
                waypoint.Power = ridedata["watts"][idx]
            if hasVelocity:
                waypoint.Speed = ridedata["velocity_smooth"][idx]
            if hasDistance:
                waypoint.Distance = ridedata["distance"][idx]
            lap.Waypoints.append(waypoint)

        return activity
Ejemplo n.º 15
0
    def DownloadActivity(self, svcRecord, activity):
        # thanks to Cosmo Catalano for the API reference code
        activityID = [
            x["ActivityID"] for x in activity.UploadedTo
            if x["Connection"] == svcRecord
        ][0]

        streamdata = requests.get(
            "https://www.strava.com/api/v3/activities/" + str(activityID) +
            "/streams/time,altitude,heartrate,cadence,watts,watts_calc,temp,resting,latlng",
            headers=self._apiHeaders(svcRecord))
        if streamdata.status_code == 401:
            raise APIException("No authorization to download activity",
                               block=True,
                               user_exception=UserException(
                                   UserExceptionType.Authorization,
                                   intervention_required=True))

        streamdata = streamdata.json()

        if "message" in streamdata and streamdata[
                "message"] == "Record Not Found":
            raise APIException("Could not find activity")

        ridedata = {}
        for stream in streamdata:
            ridedata[stream["type"]] = stream["data"]

        activity.Waypoints = []

        hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0
        hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0
        hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0
        hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0)
        hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0
        hasRestingData = "resting" in ridedata and len(ridedata["resting"]) > 0
        moving = True

        if "error" in ridedata:
            raise APIException("Strava error " + ridedata["error"])

        hasLocation = False
        waypointCt = len(ridedata["time"])
        for idx in range(0, waypointCt - 1):
            latlng = ridedata["latlng"][idx]

            waypoint = Waypoint(activity.StartTime +
                                timedelta(0, ridedata["time"][idx]))
            latlng = ridedata["latlng"][idx]
            waypoint.Location = Location(latlng[0], latlng[1], None)
            if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0:
                waypoint.Location.Longitude = None
                waypoint.Location.Latitude = None
            else:  # strava only returns 0 as invalid coords, so no need to check for null (update: ??)
                hasLocation = True
            if hasAltitude:
                waypoint.Location.Altitude = float(ridedata["altitude"][idx])

            if idx == 0:
                waypoint.Type = WaypointType.Start
            elif idx == waypointCt - 2:
                waypoint.Type = WaypointType.End
            elif hasRestingData and not moving and ridedata["resting"][
                    idx] is False:
                waypoint.Type = WaypointType.Resume
                moving = True
            elif hasRestingData and ridedata["resting"][idx] is True:
                waypoint.Type = WaypointType.Pause
                moving = False

            if hasHR:
                waypoint.HR = ridedata["heartrate"][idx]
            if hasCadence:
                waypoint.Cadence = ridedata["cadence"][idx]
            if hasTemp:
                waypoint.Temp = ridedata["temp"][idx]
            if hasPower:
                waypoint.Power = ridedata["watts"][idx]
            activity.Waypoints.append(waypoint)
        if not hasLocation:
            raise APIExcludeActivity("No waypoints with location",
                                     activityId=activityID)
        return activity
Ejemplo n.º 16
0
def raise_api_exception():
    raise APIException(
        "Token expired or revoked", block=True,
        user_exception=UserException(UserExceptionType.Authorization,
                                     intervention_required=True))
Ejemplo n.º 17
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)
Ejemplo n.º 18
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        activities = []
        session = self._get_session(record=serviceRecord)
        session.headers.update({"Accept": "application/json"})
        workouts_resp = session.get(
            "https://api.trainerroad.com/api/careerworkouts")

        if workouts_resp.status_code != 200:
            if workouts_resp.status_code == 401:
                raise APIException("Invalid login",
                                   block=True,
                                   user_exception=UserException(
                                       UserExceptionType.Authorization,
                                       intervention_required=True))
            raise APIException("Workout listing error")

        cached_record = cachedb.trainerroad_meta.find_one(
            {"ExternalID": serviceRecord.ExternalID})
        if not cached_record:
            cached_workout_meta = {}
        else:
            cached_workout_meta = cached_record["Workouts"]

        workouts = workouts_resp.json()
        for workout in workouts:
            # Un/f their API doesn't provide the start/end times in the list response
            # So we need to pull the extra data, if it's not already cached
            workout_id = str(workout["Id"])  # Mongo doesn't do non-string keys
            if workout_id not in cached_workout_meta:
                meta_resp = session.get(
                    "https://api.trainerroad.com/api/careerworkouts?guid=%s" %
                    workout["Guid"])
                # We don't need everything
                full_meta = meta_resp.json()
                meta = {
                    key: full_meta[key]
                    for key in [
                        "WorkoutDate", "WorkoutName", "WorkoutNotes",
                        "TotalMinutes", "TotalKM", "AvgWatts", "Kj"
                    ]
                }
                cached_workout_meta[workout_id] = meta
            else:
                meta = cached_workout_meta[workout_id]

            activity = UploadedActivity()
            activity.ServiceData = {"ID": int(workout_id)}
            activity.Name = meta["WorkoutName"]
            activity.Notes = meta["WorkoutNotes"]
            activity.Type = ActivityType.Cycling

            # Everything's in UTC
            activity.StartTime = dateutil.parser.parse(
                meta["WorkoutDate"]).replace(tzinfo=pytz.utc)
            activity.EndTime = activity.StartTime + timedelta(
                minutes=meta["TotalMinutes"])

            activity.Stats.Distance = ActivityStatistic(
                ActivityStatisticUnit.Kilometers, value=meta["TotalKM"])
            activity.Stats.Power = ActivityStatistic(
                ActivityStatisticUnit.Watts, avg=meta["AvgWatts"])
            activity.Stats.Energy = ActivityStatistic(
                ActivityStatisticUnit.Kilojoules, value=meta["Kj"])

            activity.Stationary = False
            activity.GPS = False
            activity.CalculateUID()

            activities.append(activity)

        cachedb.trainerroad_meta.update(
            {"ExternalID": serviceRecord.ExternalID}, {
                "ExternalID": serviceRecord.ExternalID,
                "Workouts": cached_workout_meta
            },
            upsert=True)

        return activities, []
Ejemplo n.º 19
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        #http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities?&start=0&limit=50
        page = 1
        pageSz = 100
        activities = []
        exclusions = []
        while True:
            logger.debug("Req with " + str({
                "start": (page - 1) * pageSz,
                "limit": pageSz
            }))

            res = self._request_with_reauth(
                serviceRecord, lambda session: session.get(
                    "https://connect.garmin.com/modern/proxy/activity-search-service-1.0/json/activities",
                    params={
                        "start": (page - 1) * pageSz,
                        "limit": pageSz
                    }))

            try:
                res = res.json()["results"]
            except ValueError:
                res_txt = res.text  # So it can capture in the log message
                raise APIException("Parse failure in GC list resp: %s - %s" %
                                   (res.status_code, res.text))
            if "activities" not in res:
                break  # No activities on this page - empty account.
            for act in res["activities"]:
                act = act["activity"]
                activity = UploadedActivity()

                # Don't really know why sumSampleCountTimestamp doesn't appear in swim activities - they're definitely timestamped...
                activity.Stationary = "sumSampleCountSpeed" not in act and "sumSampleCountTimestamp" not in act
                activity.GPS = "endLatitude" in act

                activity.Private = act["privacy"]["key"] == "private"

                try:
                    activity.TZ = pytz.timezone(act["activityTimeZone"]["key"])
                except pytz.exceptions.UnknownTimeZoneError:
                    activity.TZ = pytz.FixedOffset(
                        float(act["activityTimeZone"]["offset"]) * 60)

                logger.debug("Name " + act["activityName"]["value"] + ":")
                if len(act["activityName"]["value"].strip(
                )) and act["activityName"][
                        "value"] != "Untitled":  # This doesn't work for internationalized accounts, oh well.
                    activity.Name = act["activityName"]["value"]

                if len(act["activityDescription"]["value"].strip()):
                    activity.Notes = act["activityDescription"]["value"]

                # beginTimestamp/endTimestamp is in UTC
                activity.StartTime = pytz.utc.localize(
                    datetime.utcfromtimestamp(
                        float(act["beginTimestamp"]["millis"]) / 1000))
                if "sumElapsedDuration" in act:
                    activity.EndTime = activity.StartTime + timedelta(
                        0, round(float(act["sumElapsedDuration"]["value"])))
                elif "sumDuration" in act:
                    activity.EndTime = activity.StartTime + timedelta(
                        minutes=float(act["sumDuration"]
                                      ["minutesSeconds"].split(":")[0]),
                        seconds=float(act["sumDuration"]
                                      ["minutesSeconds"].split(":")[1]))
                else:
                    activity.EndTime = pytz.utc.localize(
                        datetime.utcfromtimestamp(
                            float(act["endTimestamp"]["millis"]) / 1000))
                logger.debug("Activity s/t " + str(activity.StartTime) +
                             " on page " + str(page))
                activity.AdjustTZ()

                if "sumDistance" in act and float(
                        act["sumDistance"]["value"]) != 0:
                    activity.Stats.Distance = ActivityStatistic(
                        self._unitMap[act["sumDistance"]["uom"]],
                        value=float(act["sumDistance"]["value"]))

                if "device" in act and act["device"]["key"] != "unknown":
                    devId = DeviceIdentifier.FindMatchingIdentifierOfType(
                        DeviceIdentifierType.GC, {"Key": act["device"]["key"]})
                    ver_split = act["device"]["key"].split(".")
                    ver_maj = None
                    ver_min = None
                    if len(ver_split) == 4:
                        # 2.90.0.0
                        ver_maj = int(ver_split[0])
                        ver_min = int(ver_split[1])
                    activity.Device = Device(devId,
                                             verMaj=ver_maj,
                                             verMin=ver_min)

                activity.Type = self._resolveActivityType(
                    act["activityType"]["key"])

                activity.CalculateUID()

                activity.ServiceData = {"ActivityID": int(act["activityId"])}

                activities.append(activity)
            logger.debug("Finished page " + str(page) + " of " +
                         str(res["search"]["totalPages"]))
            if not exhaustive or int(res["search"]["totalPages"]) == page:
                break
            else:
                page += 1
        return activities, exclusions
Ejemplo n.º 20
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        #https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?limit=20&start=0
        page = 1
        pageSz = 100
        activities = []
        exclusions = []
        while True:
            logger.debug("Req with " + str({
                "start": (page - 1) * pageSz,
                "limit": pageSz
            }))

            res = self._request_with_reauth(
                lambda session: session.get(
                    "https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities",
                    params={
                        "start": (page - 1) * pageSz,
                        "limit": pageSz
                    }), serviceRecord)

            try:
                res = res.json()
            except ValueError:
                res_txt = res.text  # So it can capture in the log message
                raise APIException("Parse failure in GC list resp: %s - %s" %
                                   (res.status_code, res_txt))
            for act in res:
                activity = UploadedActivity()
                # stationary activities have movingDuration = None while non-gps static activities have 0.0
                activity.Stationary = act["movingDuration"] is None
                activity.GPS = act["hasPolyline"]

                activity.Private = act["privacy"]["typeKey"] == "private"

                activity_name = act["activityName"]
                logger.debug("Name " + activity_name if activity_name
                             is not None else "Untitled" + ":")
                if activity_name is not None and len(
                        activity_name.strip()
                ) and activity_name != "Untitled":  # This doesn't work for internationalized accounts, oh well.
                    activity.Name = activity_name

                activity_description = act["description"]
                if activity_description is not None and len(
                        activity_description.strip()):
                    activity.Notes = activity_description

                activity.StartTime = pytz.utc.localize(
                    datetime.strptime(act["startTimeGMT"],
                                      "%Y-%m-%d %H:%M:%S"))
                if act["elapsedDuration"] is not None:
                    activity.EndTime = activity.StartTime + timedelta(
                        0,
                        float(act["elapsedDuration"]) / 1000)
                elif act["duration"] is not None:
                    activity.EndTime = activity.StartTime + timedelta(
                        0, float(act["duration"]))
                else:
                    # somehow duration is not defined. Set 1 second then.
                    activity.EndTime = activity.StartTime + timedelta(0, 1)

                logger.debug("Activity s/t " + str(activity.StartTime) +
                             " on page " + str(page))

                if "distance" in act and act["distance"] and float(
                        act["distance"]) != 0:
                    activity.Stats.Distance = ActivityStatistic(
                        ActivityStatisticUnit.Meters,
                        value=float(act["distance"]))

                activity.Type = self._resolveActivityType(
                    act["activityType"]["typeKey"])

                activity.CalculateUID()

                activity.ServiceData = {"ActivityID": int(act["activityId"])}

                activities.append(activity)
            logger.debug("Finished page " + str(page))
            if not exhaustive or len(res) == 0:
                break
            else:
                page += 1
        return activities, exclusions
Ejemplo n.º 21
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/proxy/activity-service-1.3/json/activityDetails/####
        activityID = activity.ServiceData["ActivityID"]
        res = self._request_with_reauth(
            serviceRecord, lambda session: session.
            get("https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/"
                + str(activityID) + "?maxSize=999999999"))
        try:
            raw_data = res.json(
            )["com.garmin.activity.details.json.ActivityDetails"]
        except ValueError:
            raise APIException("Activity data parse error for %s: %s" %
                               (res.status_code, res.text))

        if "measurements" not in raw_data:
            activity.Stationary = True  # We were wrong, oh well
            return activity

        attrs_map = {}

        def _map_attr(gc_key,
                      wp_key,
                      units,
                      in_location=False,
                      is_timestamp=False):
            attrs_map[gc_key] = {
                "key": wp_key,
                "to_units": units,
                "in_location": in_location,  # Blegh
                "is_timestamp": is_timestamp  # See above
            }

        _map_attr("directSpeed", "Speed",
                  ActivityStatisticUnit.MetersPerSecond)
        _map_attr("sumDistance", "Distance", ActivityStatisticUnit.Meters)
        _map_attr("directHeartRate", "HR",
                  ActivityStatisticUnit.BeatsPerMinute)
        _map_attr("directBikeCadence", "Cadence",
                  ActivityStatisticUnit.RevolutionsPerMinute)
        _map_attr("directDoubleCadence", "RunCadence",
                  ActivityStatisticUnit.StepsPerMinute)  # 2*x mystery solved
        _map_attr("directAirTemperature", "Temp",
                  ActivityStatisticUnit.DegreesCelcius)
        _map_attr("directPower", "Power", ActivityStatisticUnit.Watts)
        _map_attr("directElevation",
                  "Altitude",
                  ActivityStatisticUnit.Meters,
                  in_location=True)
        _map_attr("directLatitude", "Latitude", None, in_location=True)
        _map_attr("directLongitude", "Longitude", None, in_location=True)
        _map_attr("directTimestamp", "Timestamp", None, is_timestamp=True)

        # Figure out which metrics we'll be seeing in this activity
        attrs_indexed = {}
        for measurement in raw_data["measurements"]:
            key = measurement["key"]
            if key in attrs_map:
                if attrs_map[key]["to_units"]:
                    attrs_map[key]["from_units"] = self._unitMap[
                        measurement["unit"]]
                    if attrs_map[key]["to_units"] == attrs_map[key][
                            "from_units"]:
                        attrs_map[key]["to_units"] = attrs_map[key][
                            "from_units"] = None
                attrs_indexed[measurement["metricsIndex"]] = attrs_map[key]

        # Process the data frames
        frame_idx = 0
        active_lap_idx = 0
        for frame in raw_data["metrics"]:
            wp = Waypoint()
            for idx, attr in attrs_indexed.items():
                value = frame["metrics"][idx]
                target_obj = wp
                if attr["in_location"]:
                    if not wp.Location:
                        wp.Location = Location()
                    target_obj = wp.Location

                # Handle units
                if attr["is_timestamp"]:
                    value = pytz.utc.localize(
                        datetime.utcfromtimestamp(value / 1000))
                elif attr["to_units"]:
                    value = ActivityStatistic.convertValue(
                        value, attr["from_units"], attr["to_units"])

                # Write the value (can't use __dict__ because __slots__)
                setattr(target_obj, attr["key"], value)

            # Fix up lat/lng being zero (which appear to represent missing coords)
            if wp.Location and wp.Location.Latitude == 0 and wp.Location.Longitude == 0:
                wp.Location.Latitude = None
                wp.Location.Longitude = None
            # Please visit a physician before complaining about this
            if wp.HR == 0:
                wp.HR = None
            # Bump the active lap if required
            while (active_lap_idx < len(activity.Laps) - 1
                   and  # Not the last lap
                   activity.Laps[active_lap_idx + 1].StartTime <= wp.Timestamp
                   ):
                active_lap_idx += 1
            activity.Laps[active_lap_idx].Waypoints.append(wp)
            frame_idx += 1

        return activity
Ejemplo n.º 22
0
    def _downloadActivitySummary(self, serviceRecord, activity):
        activityID = activity.ServiceData["ActivityID"]

        summary_resp = self._request_with_reauth(
            lambda session: session.
            get("https://connect.garmin.com/modern/proxy/activity-service/activity/"
                + str(activityID)), serviceRecord)

        try:
            summary_data = summary_resp.json()
        except ValueError:
            raise APIException("Failure downloading activity summary %s:%s" %
                               (summary_resp.status_code, summary_resp.text))
        stat_map = {}

        def mapStat(gcKey, statKey, type, units):
            stat_map[gcKey] = {"key": statKey, "attr": type, "units": units}

        def applyStats(gc_dict, stats_obj):
            for gc_key, stat in stat_map.items():
                if gc_key in gc_dict:
                    value = float(gc_dict[gc_key])
                    if math.isinf(value):
                        continue  # GC returns the minimum speed as "-Infinity" instead of 0 some times :S
                    getattr(stats_obj, stat["key"]).update(
                        ActivityStatistic(stat["units"],
                                          **({
                                              stat["attr"]: value
                                          })))

        mapStat("movingDuration", "MovingTime", "value",
                ActivityStatisticUnit.Seconds)
        mapStat("duration", "TimerTime", "value",
                ActivityStatisticUnit.Seconds)
        mapStat("distance", "Distance", "value", ActivityStatisticUnit.Meters)
        mapStat("maxSpeed", "Speed", "max",
                ActivityStatisticUnit.MetersPerSecond)
        mapStat("averageSpeed", "Speed", "avg",
                ActivityStatisticUnit.MetersPerSecond)
        mapStat("calories", "Energy", "value",
                ActivityStatisticUnit.Kilocalories)
        mapStat("maxHR", "HR", "max", ActivityStatisticUnit.BeatsPerMinute)
        mapStat("averageHR", "HR", "avg", ActivityStatisticUnit.BeatsPerMinute)
        mapStat("minElevation", "Elevation", "min",
                ActivityStatisticUnit.Meters)
        mapStat("maxElevation", "Elevation", "max",
                ActivityStatisticUnit.Meters)
        mapStat("elevationGain", "Elevation", "gain",
                ActivityStatisticUnit.Meters)
        mapStat("elevationLoss", "Elevation", "loss",
                ActivityStatisticUnit.Meters)
        mapStat("averageBikeCadence", "Cadence", "avg",
                ActivityStatisticUnit.RevolutionsPerMinute)
        mapStat("averageCadence", "Cadence", "avg",
                ActivityStatisticUnit.StepsPerMinute)

        applyStats(summary_data["summaryDTO"], activity.Stats)

        laps_resp = self._request_with_reauth(
            lambda session: session.
            get("https://connect.garmin.com/modern/proxy/activity-service/activity/%s/splits"
                % str(activityID)), serviceRecord)
        try:
            laps_data = laps_resp.json()
        except ValueError:
            raise APIException(
                "Failure downloading activity laps summary %s:%s" %
                (laps_resp.status_code, laps_resp.text))

        for lap_data in laps_data["lapDTOs"]:
            lap = Lap()
            if "startTimeGMT" in lap_data:
                lap.StartTime = pytz.utc.localize(
                    datetime.strptime(lap_data["startTimeGMT"],
                                      "%Y-%m-%dT%H:%M:%S.0"))

            elapsed_duration = None
            if "elapsedDuration" in lap_data:
                elapsed_duration = timedelta(
                    seconds=round(float(lap_data["elapsedDuration"])))
            elif "duration" in lap_data:
                elapsed_duration = timedelta(
                    seconds=round(float(lap_data["duration"])))

            if lap.StartTime and elapsed_duration:
                # Always recalculate end time based on duration, if we have the start time
                lap.EndTime = lap.StartTime + elapsed_duration
            if not lap.StartTime and lap.EndTime and elapsed_duration:
                # Sometimes calculate start time based on duration
                lap.StartTime = lap.EndTime - elapsed_duration

            if not lap.StartTime or not lap.EndTime:
                # Garmin Connect is weird.
                raise APIExcludeActivity(
                    "Activity lap has no BeginTimestamp or EndTimestamp",
                    user_exception=UserException(UserExceptionType.Corrupt))

            applyStats(lap_data, lap.Stats)
            activity.Laps.append(lap)

        # In Garmin Land, max can be smaller than min for this field :S
        if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max:
            activity.Stats.Power.Min = None
Ejemplo n.º 23
0
    def UploadActivity(self, serviceRecord, activity):
        metrics = {
            "data": [],
            "metricTypes": [],
            "intervalUnit": "SEC",
            "intervalValue":
            10 if activity.Type == ActivityType.Running else 5  # What a joke.
        }

        act = [{
            "deviceName":
            "tapiriik",
            "deviceType":
            "BIKE" if activity.Type == ActivityType.Cycling else
            "WATCH",  # ??? nike+ is weird
            "startTime":
            calendar.timegm(
                activity.StartTime.astimezone(pytz.utc).timetuple()) * 1000,
            "timeZoneName":
            str(activity.TZ),
            "activityType": [
                k for k, v in self._reverseActivityMappings.items()
                if v == activity.Type
            ][0],
            "metrics":
            metrics
        }]

        wps = activity.GetFlatWaypoints()
        wpidx = 0
        full_metrics = []
        max_metrics = set()
        while True:
            wp = wps[wpidx]
            my_metrics = {}

            if wp.Location and wp.Location.Latitude is not None and wp.Location.Longitude is not None:
                elev = wp.Location.Altitude if wp.Location.Altitude else 0  # They always require this field, it's meh
                my_metrics.update({
                    "latitude": wp.Location.Latitude,
                    "longitude": wp.Location.Longitude,
                    "elevation": elev
                })

            if wp.Distance is not None:
                my_metrics["distance"] = wp.Distance / 1000  # m -> km

            if wp.HR is not None:
                my_metrics["heartrate"] = round(wp.HR)

            if wp.Speed is not None:
                my_metrics["speed"] = wp.Speed

            if wp.Calories is not None:
                my_metrics["calories"] = round(wp.Calories)

            if wp.Power is not None:
                my_metrics["watts"] = round(wp.Power)

            max_metrics |= my_metrics.keys()
            full_metrics.append(my_metrics)

            # Skip to next wp
            skip_delta = 0
            while (wpidx + skip_delta < len(wps) - 1) and (
                    wps[wpidx + skip_delta].Timestamp - wps[wpidx].Timestamp
            ).total_seconds() < metrics["intervalValue"]:
                skip_delta += 1
            if skip_delta == 0:
                break  # We're done
            wpidx += skip_delta

        if wpidx == 0 and len(wps) > 0:
            raise Exception("Activity had waypoints, none were used")

        max_metrics = sorted(list(max_metrics))
        metrics["metricTypes"] = max_metrics

        # Passing null metric values makes Nike+ sad
        # So we hold the last value until a new one is available
        frame_hold = {x: 0 for x in max_metrics}  # Blegh, close enough
        for metric_frame in full_metrics:
            frame_hold.update(metric_frame)
            metrics["data"].append([frame_hold[x] for x in max_metrics])

        headers = {"Content-Type": "application/json"}

        session = self._get_session(serviceRecord)
        upload_resp = session.post("https://api.nike.com/me/sport/activities",
                                   params=self._with_auth(session),
                                   data=json.dumps(act),
                                   headers=headers)

        if upload_resp.status_code != 201:
            error_codes = [x["code"] for x in upload_resp.json()["errors"]]
            if 320 in error_codes:  # Invalid combination of metric types and blah blah blah
                raise APIException("Not enough data, have keys %s" %
                                   max_metrics,
                                   user_exception=UserException(
                                       UserExceptionType.InsufficientData))
            raise APIException("Could not upload activity %s - %s" %
                               (upload_resp.status_code, upload_resp.text))

        return upload_resp.json()[0]["activityId"]
Ejemplo n.º 24
0
    def DownloadActivity(self, svcRecord, activity):
        activityID = activity.ServiceData["ActivityID"]

        logging.info("\t\t DC LOADING  : " + str(activityID))

        headers = self._getAuthHeaders(svcRecord)
        self._rate_limit()
        resp = requests.get(DECATHLON_API_BASE_URL + "/activity/" +
                            activityID + "/fullactivity.xml",
                            headers=headers)

        if resp.status_code == 401:
            raise APIException("No authorization to download activity",
                               block=True,
                               user_exception=UserException(
                                   UserExceptionType.Authorization,
                                   intervention_required=True))

        try:
            root = xml.fromstring(resp.content)
        except:
            raise APIException(
                "Stream data returned from Decathlon is not XML")

        activity.GPS = False
        activity.Stationary = True
        #work on date
        startdate = root.find('.//STARTDATE').text
        timezone = root.find('.//TIMEZONE').text
        datebase = parse(startdate + timezone)

        ridedata = {}
        ridedataindex = []

        for pt in root.iter('LOCATION'):
            delta = int(pt.get('elapsed_time'))
            ridedataindex.append(delta)
            ridedata[delta] = {}
            if activityID == 'eu2132ac60d9a40a1d9a':
                logging.info('========time : ' + str(delta))
                logging.info('========lat : ' +
                             str(float(pt.find('LATITUDE').text[:8])))
            ridedata[delta]['LATITUDE'] = float(pt.find('LATITUDE').text[:8])
            ridedata[delta]['LONGITUDE'] = float(pt.find('LONGITUDE').text[:8])
            ridedata[delta]['ELEVATION'] = int(pt.find('ELEVATION').text[:8])

        if len(ridedata) > 0:
            activity.GPS = True
            activity.Stationary = False

        for measure in root.iter('MEASURE'):
            delta = int(measure.get('elapsed_time'))
            if delta not in ridedataindex:
                ridedataindex.append(delta)
                ridedata[delta] = {}

            for measureValue in measure.iter('VALUE'):
                if measureValue.get('id') == "1":
                    ridedata[delta]['HR'] = int(measureValue.text)
                if measureValue.get('id') == "6":
                    ridedata[delta]['SPEED'] = int(measureValue.text)
                if measureValue.get('id') == "5":
                    ridedata[delta]['DISTANCE'] = int(measureValue.text)
                if measureValue.get('id') == "20":
                    ridedata[delta]['LAP'] = int(measureValue.text)

        ridedataindex.sort()

        if len(ridedata) == 0:
            lap = Lap(stats=activity.Stats,
                      startTime=activity.StartTime,
                      endTime=activity.EndTime)
            activity.Laps = [lap]
        else:
            lapWaypoints = []
            startTimeLap = activity.StartTime
            for elapsedTime in ridedataindex:
                rd = ridedata[elapsedTime]
                wp = Waypoint()
                delta = elapsedTime
                formatedDate = datebase + timedelta(seconds=delta)
                wp.Timestamp = formatedDate  #self._parseDate(formatedDate.isoformat())

                if 'LATITUDE' in rd:
                    wp.Location = Location()
                    wp.Location.Latitude = rd['LATITUDE']
                    wp.Location.Longitude = rd['LONGITUDE']
                    wp.Location.Altitude = rd['ELEVATION']

                if 'HR' in rd:
                    wp.HR = rd['HR']

                if 'SPEED' in rd:
                    wp.Speed = rd['SPEED'] / 3600

                if 'DISTANCE' in rd:
                    wp.Distance = rd['DISTANCE']

                lapWaypoints.append(wp)

                if "LAP" in rd:
                    #build the lap
                    lap = Lap(stats=activity.Stats,
                              startTime=startTimeLap,
                              endTime=formatedDate)
                    lap.Waypoints = lapWaypoints
                    activity.Laps.append(lap)
                    # re init a new lap
                    startTimeLap = formatedDate
                    lapWaypoints = []

            #build last lap
            if len(lapWaypoints) > 0:
                lap = Lap(stats=activity.Stats,
                          startTime=startTimeLap,
                          endTime=formatedDate)
                lap.Waypoints = lapWaypoints
                activity.Laps.append(lap)

        return activity
Ejemplo n.º 25
0
    def DownloadActivity(self, serviceRecord, activity):
        resp = self._oauthSession(serviceRecord).get(
            "https://api.endomondo.com/api/1/workouts/%d" %
            activity.ServiceData["WorkoutID"],
            params={"fields": "points"})
        try:
            resp = resp.json()
        except ValueError:
            self._rateLimitBailout(resp)
            res_txt = resp.text
            raise APIException(
                "Parse failure in Endomondo activity download: %s" %
                resp.status_code)
        lap = Lap(stats=activity.Stats,
                  startTime=activity.StartTime,
                  endTime=activity.EndTime)
        activity.Laps = [lap]

        activity.GPS = False

        old_location = None
        in_pause = False
        for pt in resp["points"]:
            wp = Waypoint()
            if "time" not in pt:
                # Manually-entered activities with a course attached to them have date-less waypoints
                # It'd be nice to transfer those courses, but it's a concept few other sites support AFAIK
                # So, ignore the points entirely
                continue
            wp.Timestamp = self._parseDate(pt["time"])

            if ("lat" in pt and "lng" in pt) or "alt" in pt:
                wp.Location = Location()
                if "lat" in pt and "lng" in pt:
                    wp.Location.Latitude = pt["lat"]
                    wp.Location.Longitude = pt["lng"]
                    activity.GPS = True
                if "alt" in pt:
                    wp.Location.Altitude = pt["alt"]

                if wp.Location == old_location:
                    # We have seen the point with the same coordinates
                    # before. This causes other services (e.g Strava) to
                    # interpret this as if we were standing for a while,
                    # which causes us having wrong activity time when
                    # importing. We mark the point as paused in hopes this
                    # fixes the issue.
                    in_pause = True
                    wp.Type = WaypointType.Pause
                elif in_pause:
                    in_pause = False
                    wp.Type = WaypointType.Resume

                old_location = wp.Location

            if "hr" in pt:
                wp.HR = pt["hr"]

            if "cad" in pt:
                wp.Cadence = pt["cad"]

            if "pow" in pt:
                wp.Power = pt["pow"]

            lap.Waypoints.append(wp)
        activity.Stationary = len(lap.Waypoints) == 0
        return activity
Ejemplo n.º 26
0
    def UploadActivity(self, svcRecord, activity):
        logging.info("UPLOAD To Decathlon Activity tz " + str(activity.TZ) +
                     " dt tz " + str(activity.StartTime.tzinfo) +
                     " starttime " + str(activity.StartTime))

        #XML build
        root = etree.Element("ACTIVITY")
        header = etree.SubElement(root, "HEADER")
        etree.SubElement(header, "NAME").text = activity.Name
        etree.SubElement(header,
                         "DATE").text = str(activity.StartTime).replace(
                             " ", "T")
        duration = int((activity.EndTime - activity.StartTime).total_seconds())
        etree.SubElement(header, "DURATION").text = str(duration)

        etree.SubElement(
            header, "SPORTID").text = self._activityTypeMappings[activity.Type]

        etree.SubElement(header, "LDID").text = str(svcRecord.ExternalID)
        etree.SubElement(header, "MANUAL", attrib=None).text = "true"

        summary = etree.SubElement(root, "SUMMARY")
        dataSummaryDuration = etree.SubElement(summary, "VALUE")
        dataSummaryDuration.text = str(
            int((activity.EndTime - activity.StartTime).total_seconds()))
        dataSummaryDuration.attrib["id"] = self._unitMap["duration"]

        if activity.Stats.Distance.Value is not None and activity.Stats.Distance.Value > 0:
            dataSummaryDistance = etree.SubElement(summary, "VALUE")
            dataSummaryDistance.text = str((int(
                activity.Stats.Distance.asUnits(
                    ActivityStatisticUnit.Meters).Value)))
            dataSummaryDistance.attrib["id"] = self._unitMap["distance"]

        if activity.Stats.Energy.Value is not None:
            dataSummaryKcal = etree.SubElement(summary, "VALUE")
            dataSummaryKcal.text = str((int(
                activity.Stats.Energy.asUnits(
                    ActivityStatisticUnit.Kilocalories).Value)))
            dataSummaryKcal.attrib["id"] = self._unitMap["kcal"]

        if activity.Stats.HR.Average is not None and activity.Stats.HR.Average > 0:
            dataSummaryHR = etree.SubElement(summary, "VALUE")
            dataSummaryHR.text = str(int(activity.Stats.HR.Average))
            dataSummaryHR.attrib["id"] = self._unitMap["hravg"]

        #Speed average, We accept meter/hour
        if activity.Stats.Speed.Average is not None and activity.Stats.Speed.Average > 0:
            dataSummarySpeedAvg = etree.SubElement(summary, "VALUE")
            speed_kmh = activity.Stats.Speed.asUnits(
                ActivityStatisticUnit.KilometersPerHour).Average
            speed_mh = 1000 * speed_kmh

            dataSummarySpeedAvg.text = str((int(speed_mh)))
            dataSummarySpeedAvg.attrib["id"] = self._unitMap["speedaverage"]

        datameasure = etree.SubElement(root, "DATA")
        if len(activity.Laps) > 1:
            addLap = True
        else:
            addLap = False

        for lap in activity.Laps:
            for wp in lap.Waypoints:
                if wp.HR is not None or wp.Speed is not None or wp.Distance is not None or wp.Calories is not None:
                    oneMeasureLocation = etree.SubElement(
                        datameasure, "MEASURE")
                    oneMeasureLocation.attrib["elapsed_time"] = str(
                        duration -
                        int((activity.EndTime - wp.Timestamp).total_seconds()))
                    if wp.HR is not None:
                        measureHR = etree.SubElement(oneMeasureLocation,
                                                     "VALUE")
                        measureHR.text = str(int(wp.HR))
                        measureHR.attrib["id"] = self._unitMap["hrcurrent"]
                    if wp.Speed is not None:
                        measureSpeed = etree.SubElement(
                            oneMeasureLocation, "VALUE")
                        measureSpeed.text = str(int(wp.Speed * 3600))
                        measureSpeed.attrib["id"] = self._unitMap[
                            "speedcurrent"]
                    if wp.Calories is not None:
                        measureKcaletree = etree.SubElement(
                            oneMeasureLocation, "VALUE")
                        measureKcaletree.text = str(int(wp.Calories))
                        measureKcaletree.attrib["id"] = self._unitMap["kcal"]
                    if wp.Distance is not None:
                        measureDistance = etree.SubElement(
                            oneMeasureLocation, "VALUE")
                        measureDistance.text = str(int(wp.Distance))
                        measureDistance.attrib["id"] = self._unitMap[
                            "distance"]
            if addLap and oneMeasureLocation is not None:
                measureLap = etree.SubElement(oneMeasureLocation, "VALUE")
                measureLap.text = "1"
                measureLap.attrib[
                    "id"] = "20"  #add a lap here this elapsed time

        if len(activity.GetFlatWaypoints()) > 0:
            if activity.GetFlatWaypoints()[0].Location is not None:
                if activity.GetFlatWaypoints(
                )[0].Location.Latitude is not None:
                    track = etree.SubElement(root, "TRACK")
                    tracksummary = etree.SubElement(track, "SUMMARY")
                    etree.SubElement(tracksummary, "LIBELLE").text = ""
                    tracksummarylocation = etree.SubElement(
                        tracksummary, "LOCATION")
                    tracksummarylocation.attrib["elapsed_time"] = "0"
                    etree.SubElement(tracksummarylocation,
                                     "LATITUDE").text = str(
                                         activity.GetFlatWaypoints()
                                         [0].Location.Latitude)[:8]
                    etree.SubElement(tracksummarylocation,
                                     "LONGITUDE").text = str(
                                         activity.GetFlatWaypoints()
                                         [0].Location.Longitude)[:8]
                    etree.SubElement(tracksummarylocation,
                                     "ELEVATION").text = "0"

                    etree.SubElement(tracksummary, "DISTANCE").text = str(
                        int(
                            activity.Stats.Distance.asUnits(
                                ActivityStatisticUnit.Meters).Value))
                    etree.SubElement(tracksummary, "DURATION").text = str(
                        int((activity.EndTime -
                             activity.StartTime).total_seconds()))
                    etree.SubElement(
                        tracksummary,
                        "SPORTID").text = self._activityTypeMappings[
                            activity.Type]
                    etree.SubElement(tracksummary,
                                     "LDID").text = str(svcRecord.ExternalID)

                    for wp in activity.GetFlatWaypoints():
                        if wp.Location is None or wp.Location.Latitude is None or wp.Location.Longitude is None:
                            continue  # drop the point
                        #oneLocation = etree.SubElement(track, "LOCATION")
                        oneLocation = etree.SubElement(track, "LOCATION")
                        oneLocation.attrib["elapsed_time"] = str(
                            duration - int((activity.EndTime -
                                            wp.Timestamp).total_seconds()))
                        etree.SubElement(oneLocation, "LATITUDE").text = str(
                            wp.Location.Latitude)[:8]
                        etree.SubElement(oneLocation, "LONGITUDE").text = str(
                            wp.Location.Longitude)[:8]
                        if wp.Location.Altitude is not None:
                            etree.SubElement(oneLocation,
                                             "ELEVATION").text = str(
                                                 int(wp.Location.Altitude))
                        else:
                            etree.SubElement(oneLocation,
                                             "ELEVATION").text = "0"

        activityXML = etree.tostring(root,
                                     pretty_print=True,
                                     xml_declaration=True,
                                     encoding="UTF-8")

        headers = self._getAuthHeaders(svcRecord)
        self._rate_limit()
        upload_resp = requests.post(DECATHLON_API_BASE_URL +
                                    "/activity/import.xml",
                                    data=activityXML,
                                    headers=headers)

        if upload_resp.status_code != 200:
            raise APIException("Could not upload activity %s %s" %
                               (upload_resp.status_code, upload_resp.text))

        upload_id = None

        try:
            root = xml.fromstring(upload_resp.content)
            upload_id = root.find('.//ID').text
        except:
            raise APIException("Stream data returned is not XML")

        return upload_id
Ejemplo n.º 27
0
 def _rateLimitBailout(self, response):
     if response.status_code == 503 and "user_refused" in response.text:
         raise APIException("Endomondo user token rate limit reached",
                            user_exception=UserException(
                                UserExceptionType.RateLimited))
Ejemplo n.º 28
0
    def DownloadActivityList(self, svcRecord, exhaustive=False):
        activities = []
        exclusions = []
        before = earliestDate = None

        while True:
            logger.debug("Req with before=" + str(before) + "/" +
                         str(earliestDate))
            resp = requests.get("https://www.strava.com/api/v3/athletes/" +
                                str(svcRecord.ExternalID) + "/activities",
                                headers=self._apiHeaders(svcRecord),
                                params={"before": before})
            if resp.status_code == 401:
                raise APIException(
                    "No authorization to retrieve activity list",
                    block=True,
                    user_exception=UserException(
                        UserExceptionType.Authorization,
                        intervention_required=True))

            earliestDate = None

            reqdata = resp.json()

            if not len(reqdata):
                break  # No more activities to see

            for ride in reqdata:
                activity = UploadedActivity()
                activity.TZ = pytz.timezone(
                    re.sub("^\([^\)]+\)\s*", "", ride["timezone"])
                )  # Comes back as "(GMT -13:37) The Stuff/We Want""
                activity.StartTime = pytz.utc.localize(
                    datetime.strptime(ride["start_date"],
                                      "%Y-%m-%dT%H:%M:%SZ"))
                logger.debug("\tActivity s/t " + str(activity.StartTime))
                if not earliestDate or activity.StartTime < earliestDate:
                    earliestDate = activity.StartTime
                    before = calendar.timegm(
                        activity.StartTime.astimezone(pytz.utc).timetuple())

                if ride["start_latlng"] is None or ride[
                        "end_latlng"] is None or ride[
                            "distance"] is None or ride["distance"] == 0:
                    exclusions.append(
                        APIExcludeActivity("No path", activityId=ride["id"]))
                    logger.debug("\t\tNo pts")
                    continue  # stationary activity - no syncing for now

                activity.EndTime = activity.StartTime + timedelta(
                    0, ride["elapsed_time"])
                activity.UploadedTo = [{
                    "Connection": svcRecord,
                    "ActivityID": ride["id"]
                }]

                actType = [
                    k for k, v in self._reverseActivityTypeMappings.items()
                    if v == ride["type"]
                ]
                if not len(actType):
                    exclusions.append(
                        APIExcludeActivity("Unsupported activity type %s" %
                                           ride["type"],
                                           activityId=ride["id"]))
                    logger.debug("\t\tUnknown activity")
                    continue

                activity.Type = actType[0]
                activity.Distance = ride["distance"]
                activity.Name = ride["name"]
                activity.Private = ride["private"]
                activity.AdjustTZ()
                activity.CalculateUID()
                activities.append(activity)

            if not exhaustive or not earliestDate:
                break

        return activities, exclusions
Ejemplo n.º 29
0
    def DownloadActivity(self, svcRecord, activity):
        activityID = activity.ServiceData["ActivityID"]

        logger.info("\t\t DC LOADING  : " + str(activityID))

        headers = self._getAuthHeaders(svcRecord)
        resp = requests.get(self.ApiEndpoint + "/activity/" + activityID +
                            "/fullactivity.xml",
                            headers=headers)
        if resp.status_code == 401:
            raise APIException("No authorization to download activity",
                               block=True,
                               user_exception=UserException(
                                   UserExceptionType.Authorization,
                                   intervention_required=True))

        try:
            root = xml.fromstring(resp.content)
        except:
            raise APIException(
                "Stream data returned from DecathlonCoach is not XML")

        lap = Lap(stats=activity.Stats,
                  startTime=activity.StartTime,
                  endTime=activity.EndTime)
        activity.Laps = [lap]
        lap.Waypoints = []

        activity.GPS = False

        #work on date
        startdate = root.find('.//STARTDATE').text
        timezone = root.find('.//TIMEZONE').text
        datebase = parse(startdate + timezone)

        for pt in root.iter('LOCATION'):
            wp = Waypoint()

            delta = int(pt.get('elapsed_time'))
            formatedDate = datebase + timedelta(seconds=delta)

            wp.Timestamp = formatedDate  #self._parseDate(formatedDate.isoformat())

            wp.Location = Location()
            wp.Location.Latitude = float(pt.find('LATITUDE').text[:8])
            wp.Location.Longitude = float(pt.find('LONGITUDE').text[:8])
            activity.GPS = True
            wp.Location.Altitude = int(pt.find('ELEVATION').text[:8])

            #get the HR value in the Datastream node and measures collection
            for hr in root.iter('MEASURE'):
                if pt.get('elapsed_time') == hr.get('elapsed_time'):
                    for measureValue in hr.iter('VALUE'):
                        if measureValue.get('id') == "1":
                            wp.HR = int(measureValue.text)
                            break
                    break

            lap.Waypoints.append(wp)
        activity.Stationary = len(lap.Waypoints) == 0

        return activity
Ejemplo n.º 30
0
    def UploadActivity(self, serviceRecord, activity):
        activityData = {}
        # Props to the SportTracks API people for seamlessly supprting activities with or without TZ data.
        activityData["start_time"] = activity.StartTime.isoformat()
        if activity.Name:
            activityData["name"] = activity.Name
        if activity.Notes:
            activityData["notes"] = activity.Notes
        activityData[
            "sharing"] = "public" if not activity.Private else "private"
        activityData["type"] = self._reverseActivityMappings[activity.Type]

        def _resolveDuration(obj):
            if obj.Stats.TimerTime.Value is not None:
                return obj.Stats.TimerTime.asUnits(
                    ActivityStatisticUnit.Seconds).Value
            if obj.Stats.MovingTime.Value is not None:
                return obj.Stats.MovingTime.asUnits(
                    ActivityStatisticUnit.Seconds).Value
            return (obj.EndTime - obj.StartTime).total_seconds()

        def _mapStat(dict, key, val, naturalValue=False):
            if val is not None:
                if naturalValue:
                    val = round(val)
                dict[key] = val

        _mapStat(activityData, "clock_duration",
                 (activity.EndTime - activity.StartTime).total_seconds())
        _mapStat(
            activityData, "duration", _resolveDuration(activity)
        )  # This has to be set, otherwise all time shows up as "stopped" :(
        _mapStat(
            activityData, "total_distance",
            activity.Stats.Distance.asUnits(
                ActivityStatisticUnit.Meters).Value)
        _mapStat(activityData,
                 "calories",
                 activity.Stats.Energy.asUnits(
                     ActivityStatisticUnit.Kilojoules).Value,
                 naturalValue=True)
        _mapStat(activityData, "elevation_gain", activity.Stats.Elevation.Gain)
        _mapStat(activityData, "elevation_loss", activity.Stats.Elevation.Loss)
        _mapStat(activityData, "max_speed", activity.Stats.Speed.Max)
        _mapStat(activityData, "avg_heartrate", activity.Stats.HR.Average)
        _mapStat(activityData, "max_heartrate", activity.Stats.HR.Max)
        _mapStat(activityData, "avg_cadence", activity.Stats.Cadence.Average)
        _mapStat(activityData, "max_cadence", activity.Stats.Cadence.Max)
        _mapStat(activityData, "avg_power", activity.Stats.Power.Average)
        _mapStat(activityData, "max_power", activity.Stats.Power.Max)

        activityData["laps"] = []
        lapNum = 0
        for lap in activity.Laps:
            lapNum += 1
            lapinfo = {
                "number": lapNum,
                "start_time": lap.StartTime.isoformat(),
                "type":
                "REST" if lap.Intensity == LapIntensity.Rest else "ACTIVE"
            }
            _mapStat(lapinfo, "clock_duration",
                     (lap.EndTime -
                      lap.StartTime).total_seconds())  # Required too.
            _mapStat(lapinfo, "duration", _resolveDuration(
                lap))  # This field is required for laps to be created.
            _mapStat(
                lapinfo, "distance",
                lap.Stats.Distance.asUnits(
                    ActivityStatisticUnit.Meters).Value)  # Probably required.
            _mapStat(lapinfo,
                     "calories",
                     lap.Stats.Energy.asUnits(
                         ActivityStatisticUnit.Kilojoules).Value,
                     naturalValue=True)
            _mapStat(lapinfo, "elevation_gain", lap.Stats.Elevation.Gain)
            _mapStat(lapinfo, "elevation_loss", lap.Stats.Elevation.Loss)
            _mapStat(lapinfo, "max_speed", lap.Stats.Speed.Max)
            _mapStat(lapinfo, "avg_heartrate", lap.Stats.HR.Average)
            _mapStat(lapinfo, "max_heartrate", lap.Stats.HR.Max)

            activityData["laps"].append(lapinfo)
        if not activity.Stationary:
            timer_stops = []
            timer_stopped_at = None

            def stream_append(stream, wp, data):
                stream += [
                    round((wp.Timestamp - activity.StartTime).total_seconds()),
                    data
                ]

            location_stream = []
            distance_stream = []
            elevation_stream = []
            heartrate_stream = []
            power_stream = []
            cadence_stream = []
            for lap in activity.Laps:
                for wp in lap.Waypoints:
                    if wp.Location and wp.Location.Latitude and wp.Location.Longitude:
                        stream_append(
                            location_stream, wp,
                            [wp.Location.Latitude, wp.Location.Longitude])
                    if wp.HR:
                        stream_append(heartrate_stream, wp, round(wp.HR))
                    if wp.Distance:
                        stream_append(distance_stream, wp, wp.Distance)
                    if wp.Cadence or wp.RunCadence:
                        stream_append(
                            cadence_stream, wp,
                            round(wp.Cadence)
                            if wp.Cadence else round(wp.RunCadence))
                    if wp.Power:
                        stream_append(power_stream, wp, wp.Power)
                    if wp.Location and wp.Location.Altitude:
                        stream_append(elevation_stream, wp,
                                      wp.Location.Altitude)
                    if wp.Type == WaypointType.Pause and not timer_stopped_at:
                        timer_stopped_at = wp.Timestamp
                    if wp.Type != WaypointType.Pause and timer_stopped_at:
                        timer_stops.append([timer_stopped_at, wp.Timestamp])
                        timer_stopped_at = None

            activityData["elevation"] = elevation_stream
            activityData["heartrate"] = heartrate_stream
            activityData["power"] = power_stream
            activityData["cadence"] = cadence_stream
            activityData["distance"] = distance_stream
            activityData["location"] = location_stream
            activityData["timer_stops"] = [[y.isoformat() for y in x]
                                           for x in timer_stops]

        headers = self._getAuthHeaders(serviceRecord)
        headers.update({"Content-Type": "application/json"})
        upload_resp = requests.post(self.OpenFitEndpoint +
                                    "/fitnessActivities.json",
                                    data=json.dumps(activityData),
                                    headers=headers)
        if upload_resp.status_code != 200:
            if upload_resp.status_code == 401:
                raise APIException("ST.mobi trial expired",
                                   block=True,
                                   user_exception=UserException(
                                       UserExceptionType.AccountExpired,
                                       intervention_required=True))
            raise APIException("Unable to upload activity %s" %
                               upload_resp.text)
        return upload_resp.json()["uris"][0]