From 1328ede6a1ccbb4eb7ed4a0e86e950750a48ccc2 Mon Sep 17 00:00:00 2001 From: dprebyl Date: Tue, 11 May 2021 10:50:45 -0500 Subject: [PATCH] Add kml support --- garminexport/backup.py | 17 ++++++++++++++--- garminexport/garminclient.py | 29 +++++++++++++++++++++++++++-- garminexport/incremental_backup.py | 2 +- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/garminexport/backup.py b/garminexport/backup.py index 86348a21..c34dc388 100644 --- a/garminexport/backup.py +++ b/garminexport/backup.py @@ -8,13 +8,14 @@ log = logging.getLogger(__name__) -supported_export_formats = ["json_summary", "json_details", "gpx", "tcx", "fit"] +supported_export_formats = ["json_summary", "json_details", "gpx", "kml", "tcx", "fit"] """The range of supported export formats for activities.""" format_suffix = { "json_summary": "_summary.json", "json_details": "_details.json", "gpx": ".gpx", + "kml": ".kml", "tcx": ".tcx", "fit": ".fit" } @@ -64,7 +65,7 @@ def need_backup(activities, backup_dir, export_formats=None): :param backup_dir: Destination directory for exported activities. :type backup_dir: str :keyword export_formats: Which format(s) to export to. Could be any - of: 'json_summary', 'json_details', 'gpx', 'tcx', 'fit'. + of: 'json_summary', 'json_details', 'gpx', 'kml', 'tcx', 'fit'. :type export_formats: list of str :return: All activities that need to be backed up. :rtype: set of tuples of `(int, datetime)` @@ -111,7 +112,7 @@ def download(client, activity, retryer, backup_dir, export_formats=None): :param backup_dir: Backup directory path (assumed to exist already). :type backup_dir: str :keyword export_formats: Which format(s) to export to. Could be any - of: 'json_summary', 'json_details', 'gpx', 'tcx', 'fit'. + of: 'json_summary', 'json_details', 'gpx', 'kml', 'tcx', 'fit'. :type export_formats: list of str """ id = activity[0] @@ -144,6 +145,16 @@ def download(client, activity, retryer, backup_dir, export_formats=None): with codecs.open(dest, encoding="utf-8", mode="w") as f: f.write(activity_gpx) + if 'kml' in export_formats: + log.debug("getting kml for %s", id) + activity_kml = retryer.call(client.get_activity_kml, id) + dest = os.path.join(backup_dir, export_filename(activity, 'kml')) + if activity_kml is None: + not_found.write(os.path.basename(dest) + "\n") + else: + with codecs.open(dest, encoding="utf-8", mode="w") as f: + f.write(activity_kml) + if 'tcx' in export_formats: log.debug("getting tcx for %s", id) activity_tcx = retryer.call(client.get_activity_tcx, id) diff --git a/garminexport/garminclient.py b/garminexport/garminclient.py index e2210091..0ae906f2 100755 --- a/garminexport/garminclient.py +++ b/garminexport/garminclient.py @@ -325,6 +325,31 @@ def get_activity_gpx(self, activity_id): activity_id, response.status_code, response.text)) return response.text + @require_session + def get_activity_gpx(self, activity_id): + """Return a KML (Keyhole Markup Language) representation of a + given activity. If the activity cannot be exported to KML + (not yet observed in practice, but that doesn't exclude the + possibility), a :obj:`None` value is returned. + + :param activity_id: Activity identifier. + :type activity_id: int + :returns: The KML representation of the activity as an XML string + or ``None`` if the activity couldn't be exported to KML. + :rtype: str + """ + response = self.session.get( + "https://connect.garmin.com/proxy/download-service/export/kml/activity/{}".format(activity_id)) + # A 404 (Not Found) or 204 (No Content) response are both indicators + # of a gpx file not being available for the activity. It may, for + # example be a manually entered activity without any device data. + if response.status_code in (404, 204): + return None + if response.status_code != 200: + raise Exception(u"failed to fetch KML for activity {}: {}\n{}".format( + activity_id, response.status_code, response.text)) + return response.text + @require_session def get_activity_tcx(self, activity_id): """Return a TCX (Training Center XML) representation of a @@ -433,7 +458,7 @@ def upload_activity(self, file, format=None, name=None, description=None, activi """Upload a GPX, TCX, or FIT file for an activity. :param file: Path or open file - :param format: File format (gpx, tcx, or fit); guessed from filename if :obj:`None` + :param format: File format (gpx, kml, tcx, or fit); guessed from filename if :obj:`None` :type format: str :param name: Optional name for the activity on Garmin Connect :type name: str @@ -454,7 +479,7 @@ def upload_activity(self, file, format=None, name=None, description=None, activi fn = os.path.basename(file.name) _, ext = os.path.splitext(fn) if format is None: - if ext.lower() in ('.gpx', '.tcx', '.fit'): + if ext.lower() in ('.gpx', '.kml', '.tcx', '.fit'): format = ext.lower()[1:] else: raise Exception(u"could not guess file type for {}".format(fn)) diff --git a/garminexport/incremental_backup.py b/garminexport/incremental_backup.py index 6c0f883d..01ac0ff1 100644 --- a/garminexport/incremental_backup.py +++ b/garminexport/incremental_backup.py @@ -30,7 +30,7 @@ def incremental_backup(username: str, library is used. :type user_agent_fn: Callable[[], str] :param backup_dir: Destination directory for downloaded activities. Default: ./activities/". - :param export_formats: List of desired output formats (json_summary, json_details, gpx, tcx, fit). + :param export_formats: List of desired output formats (json_summary, json_details, gpx, kml, tcx, fit). Default: `None` which means all supported formats will be backed up. :param ignore_errors: Ignore errors and keep going. Default: False. :param max_retries: The maximum number of retries to make on failed attempts to fetch an activity.