Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding get activity and update activity methods #74

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 55 additions & 20 deletions garminexport/garminclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import re
import sys
import zipfile
from datetime import timedelta, datetime
from builtins import range
from datetime import timedelta, datetime
from functools import wraps
from io import BytesIO

Expand All @@ -31,7 +31,7 @@
except (ImportError):
pass

from garminexport.retryer import Retryer, ExponentialBackoffDelayStrategy, MaxRetriesStopStrategy
from garminexport.garminexport.retryer import Retryer, ExponentialBackoffDelayStrategy, MaxRetriesStopStrategy

#
# Note: For more detailed information about the API services
Expand Down Expand Up @@ -220,20 +220,35 @@ def list_activities(self):
:rtype: tuples of (int, datetime)
"""
ids = []
for activity in self._fetch_activities():
id = int(activity["activityId"])
timestamp_utc = dateutil.parser.parse(activity["startTimeGMT"])
# make sure UTC timezone gets set
timestamp_utc = timestamp_utc.replace(tzinfo=dateutil.tz.tzutc())
ids.append((id, timestamp_utc))
return ids

@require_session
def get_activities(self):
"""Return all activities with all their information.

:return: The full list of activities, with all their information.
:rtype: list of activities info dicts.
"""
activities = []
batch_size = 100
# fetch in batches since the API doesn't allow more than a certain
# number of activities to be retrieved on every invocation
for start_index in range(0, sys.maxsize, batch_size):
next_batch = self._fetch_activity_ids_and_ts(start_index, batch_size)
if not next_batch:
activity_batch = self._fetch_activities(start_index, batch_size)
if not activity_batch:
break
ids.extend(next_batch)
return ids
activities.extend(activity_batch)
return activities

@require_session
def _fetch_activity_ids_and_ts(self, start_index, max_limit=100):
"""Return a sequence of activity ids (along with their starting
timestamps) starting at a given index, with index 0 being the user's
def _fetch_activities(self, start_index, max_limit=100):
"""Return a sequence of activities information starting at a given index, with index 0 being the user's
most recently registered activity.

Should the index be out of bounds or the account empty, an empty list is returned.
Expand All @@ -243,8 +258,8 @@ def _fetch_activity_ids_and_ts(self, start_index, max_limit=100):
:param max_limit: The (maximum) number of activities to retrieve.
:type max_limit: int

:returns: A list of activity JSON dicts describing the activity
:rtype: tuples of (int, datetime)
:return: A list of activities information.
:rtype: list of activity information dicts.
"""
log.debug("fetching activities %d through %d ...", start_index, start_index + max_limit - 1)
response = self.session.get(
Expand All @@ -259,15 +274,8 @@ def _fetch_activity_ids_and_ts(self, start_index, max_limit=100):
# index out of bounds or empty account
return []

entries = []
for activity in activities:
id = int(activity["activityId"])
timestamp_utc = dateutil.parser.parse(activity["startTimeGMT"])
# make sure UTC timezone gets set
timestamp_utc = timestamp_utc.replace(tzinfo=dateutil.tz.tzutc())
entries.append((id, timestamp_utc))
log.debug("got %d activities.", len(entries))
return entries
log.debug("got %d activities.", len(activities))
return activities

@require_session
def get_activity_summary(self, activity_id):
Expand Down Expand Up @@ -534,3 +542,30 @@ def upload_activity(self, file, format=None, name=None, description=None, activi
activity_id, response.status_code, response.text))

return activity_id

@require_session
def update_activity(self, activity_id, update_kind, update_data):
"""Update an existing activity.

:param activity_id: Activity identifier.
:type activity_id: int
:param update_kind: Kind of data to update. Can be 'activityTypeDTO' to change the type,
'activityName' to change its name, ...
:type update_kind: str
:param update_data New content. For example, to change the type of an activity, should be a dict
like {"typeKey": "indoor_cardio"}.
:type update_data depends on the kind of data to update, mainly str, int or dict.
"""

# Prepare request body
data = {update_kind: update_data, 'activityId': activity_id}

# Update
encoding_headers = {"Content-Type": "application/json; charset=UTF-8"} # see Tapiriik
response = self.session.put(
"https://connect.garmin.com/proxy/activity-service/activity/{}".format(activity_id),
data=json.dumps(data), headers=encoding_headers)
if response.status_code != 204:
raise Exception(u"failed to update {} activity {}: {}\n{}".format(
update_kind, activity_id, response.status_code, response.text))
return
27 changes: 27 additions & 0 deletions samples/count_activity_by_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import json
import logging

import dateutil.parser

from garminexport.garminexport.garminclient import GarminClient

logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)

#with GarminClient('[email protected]', 'password') as client:
# activities = client.get_activities()
with open('../../data/activities.json') as f:
activities = json.loads(f.read())
log.info("Loading %d activities", len(activities))

activities_by_type = {}
for activity in activities:
activity_type = activity['activityType']['typeKey']
if not activities_by_type.get(activity_type):
activities_by_type[activity_type] = []
activities_by_type[activity_type].append(activity)

log.info(f"Found the following activities : {activities_by_type.keys()}")

for activity_type in activities_by_type.keys():
log.info(f"Found {len(activities_by_type[activity_type])} {activity_type} activities.")
17 changes: 17 additions & 0 deletions samples/fix_untyped_activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import logging

from garminexport.garminexport.garminclient import GarminClient

logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)

with GarminClient('[email protected]', 'password') as client:
activities = client.get_activities()
log.info("num ids: %d", len(activities))

for activity in activities:
if activity['activityType']['typeKey'] == 'other':
activity_id = activity['activityId']
log.info(f"Got an untyped activity : {activity_id}")
client.update_activity(activity_id, 'activityTypeDTO', {"typeKey": "indoor_cardio"})
client.update_activity(activity_id, 'eventTypeDTO', {"typeKey": "training"})
41 changes: 41 additions & 0 deletions samples/get_swim_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
import logging

import dateutil.parser

from garminexport.garminexport.garminclient import GarminClient

logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)

#with GarminClient('[email protected]', 'password') as client:
# activities = client.get_activities()
with open('../../data/activities.json') as f:
activities = json.loads(f.read())
log.info("Loading %d activities", len(activities))

swim = []
for activity in activities:
if activity["activityType"]["typeKey"] == 'lap_swimming':
log.info("Found swim activity {}".format(dateutil.parser.parse(activity["startTimeGMT"]).date()))
swim.append(activity)

log.info(f"Found {len(swim)} swim activities")

swim_candidates = []
for activity in activities:
if activity['movingDuration'] is not None and 2400 > activity['movingDuration'] > 1000 and activity[
'distance'] is not None and 500 < activity['distance'] < 1500:
swim_candidates.append(activity)
log.info(f"Found {len(swim_candidates)} swim candidates")

mistyped = []
for activity in swim_candidates:
if activity['activityType']['typeKey'] != 'lap_swimming':
log.info("Found a mistyped swimming activity : {}".format(
dateutil.parser.parse(activity["startTimeGMT"]).date()))
mistyped.append(activity)

for activity in mistyped:
if activity['activityType']['typeKey'] == 'other':
client.update_activity(activity['activityId'], 'activityTypeDTO', {"typeKey": "lap_swimming"})
9 changes: 9 additions & 0 deletions samples/load_activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import json
import logging

logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)

with open('../../data/activities.json') as f:
activities = json.loads(f.read())
log.info("Loading %d activities", len(activities))
36 changes: 36 additions & 0 deletions samples/remove_duplicates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import json
import logging
from datetime import date

import dateutil.parser

logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)

# with GarminClient('[email protected]', 'password') as client:
# activities = client.get_activities()
with open('../../data/activities.json') as f:
activities = json.loads(f.read())
log.info("Loading %d activities", len(activities))

duplicates = []
i: int = 0
while i < len(activities):
activity_i = activities[i]
activity_i_date: date = dateutil.parser.parse(activity_i["startTimeGMT"]).date()
activity_i_type = activity_i['activityType']['typeKey']
j: int = len(activities) - 1
while j > i:
activity_j = activities[j]
activity_j_date: date = dateutil.parser.parse(activity_j["startTimeGMT"]).date()
activity_j_type = activity_j['activityType']['typeKey']
# Evaluate by date (ignoring hours)
if activity_i_date == activity_j_date and (
activity_i_type == 'lap_swimming' or activity_j_type == 'lap_swimming'):
duplicates.append([activity_i, activity_j])
j -= 1
i += 1

log.info(f"Found %d swim duplicates", len(duplicates))
for activity in duplicates:
log.info("Duplicate at {}".format(dateutil.parser.parse(activity[0]["startTimeGMT"]).date()))
6 changes: 3 additions & 3 deletions samples/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@

latest_activity, latest_activity_start = activity_ids[0]
activity = client.get_activity_summary(latest_activity)
log.info("activity id: %s", activity["activity"]["activityId"])
log.info("activity name: '%s'", activity["activity"]["activityName"])
log.info("activity description: '%s'", activity["activity"]["activityDescription"])
log.info("activity id: %s", activity["activityId"])
log.info("activity name: '%s'", activity["activityName"])
log.info("activity description: '%s'", activity["activityDescription"])
log.info(json.dumps(client.get_activity_details(latest_activity), indent=4))
log.info(client.get_activity_gpx(latest_activity))
except Exception as e:
Expand Down
13 changes: 13 additions & 0 deletions samples/store_activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import json
import logging

from garminexport.garminexport.garminclient import GarminClient

logging.basicConfig(level=logging.DEBUG, format="%(asctime)-15s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)

with GarminClient('[email protected]', 'password') as client:
activities = client.get_activities()
log.info("Stored %d activities", len(activities))
with open('../../data/activities.json', "w") as f:
f.write(json.dumps(activities, ensure_ascii=False, indent=4))