refactoring
This commit is contained in:
		
							parent
							
								
									cd5728715c
								
							
						
					
					
						commit
						bffd81a231
					
				
							
								
								
									
										36
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								README.md
									
									
									
									
									
								
							@ -1,7 +1,15 @@
 | 
				
			|||||||
garminexport
 | 
					garminexport
 | 
				
			||||||
============
 | 
					============
 | 
				
			||||||
The Garmin Connect activity exporter is a program that downloads all activities 
 | 
					The Garmin Connect activity exporter is a program that downloads *all* 
 | 
				
			||||||
for a given [Garmin Connect](http://connect.garmin.com/) account and stores them locally on the user's computer.
 | 
					activities for a given [Garmin Connect](http://connect.garmin.com/) 
 | 
				
			||||||
 | 
					account and stores them locally on the user's computer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The directory also contains an ``incremental_backup.py`` program that can be
 | 
				
			||||||
 | 
					used for incremental backups of your account. This script only downloads
 | 
				
			||||||
 | 
					activities that haven't already been downloaded to a certain backup directory.
 | 
				
			||||||
 | 
					It is typically a quicker alternative (except for the first time when all
 | 
				
			||||||
 | 
					activities will need to be downloaded).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Prerequisites
 | 
					Prerequisites
 | 
				
			||||||
=============
 | 
					=============
 | 
				
			||||||
@ -12,6 +20,7 @@ assumes that you have [Python 2.7](https://www.python.org/download/releases/2.7/
 | 
				
			|||||||
It also assumes that you have registered an account at 
 | 
					It also assumes that you have registered an account at 
 | 
				
			||||||
[Garmin Connect](http://connect.garmin.com/).
 | 
					[Garmin Connect](http://connect.garmin.com/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Getting started
 | 
					Getting started
 | 
				
			||||||
===============
 | 
					===============
 | 
				
			||||||
Create and activate a new virtual environment to create an isolated development
 | 
					Create and activate a new virtual environment to create an isolated development
 | 
				
			||||||
@ -24,16 +33,17 @@ Install the required dependencies in this virtual environment:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    pip install -r requirements.txt
 | 
					    pip install -r requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Run the program
 | 
					
 | 
				
			||||||
===============
 | 
					Running the export program
 | 
				
			||||||
The program is run as follows (use the ``--help`` flag for a list of
 | 
					==========================
 | 
				
			||||||
 | 
					The export program is run as follows (use the ``--help`` flag for a list of
 | 
				
			||||||
available options).
 | 
					available options).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ./garminexport.py <username or email>
 | 
					    ./garminexport.py <username or email>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Once started, the program will prompt you for your account password and then
 | 
					Once started, the program will prompt you for your account password and then
 | 
				
			||||||
log in to your Garmin Connect account to download all activities to a destination
 | 
					log in to your Garmin Connect account to download *all* activities to a 
 | 
				
			||||||
directory on your machine.
 | 
					destination directory on your machine.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For each activity, these files are stored: 
 | 
					For each activity, these files are stored: 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,6 +63,18 @@ Each activity file is prefixed by its upload timestamp and its
 | 
				
			|||||||
activity id.
 | 
					activity id.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Running the incremental backup program
 | 
				
			||||||
 | 
					======================================
 | 
				
			||||||
 | 
					The incremental backup program is run in a similar fashion to the export 
 | 
				
			||||||
 | 
					program (use the ``--help`` flag for a list of available options):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ./incremental_backup.py --backup-dir=activities <username or email>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In this example, it will only download activities that aren't already in
 | 
				
			||||||
 | 
					the ``activities/`` directory. Note: The incremental backup program saves
 | 
				
			||||||
 | 
					the same files for each activity as the export program (see above).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Library import
 | 
					Library import
 | 
				
			||||||
==============
 | 
					==============
 | 
				
			||||||
To install the development version of this library in your local Python 
 | 
					To install the development version of this library in your local Python 
 | 
				
			||||||
 | 
				
			|||||||
@ -3,15 +3,11 @@
 | 
				
			|||||||
and stores them locally on the user's computer.
 | 
					and stores them locally on the user's computer.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import argparse
 | 
					import argparse
 | 
				
			||||||
import codecs
 | 
					 | 
				
			||||||
from datetime import datetime
 | 
					 | 
				
			||||||
import getpass
 | 
					import getpass
 | 
				
			||||||
from garminexport.garminclient import GarminClient
 | 
					from garminexport.garminclient import GarminClient
 | 
				
			||||||
import io
 | 
					import garminexport.util
 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import shutil
 | 
					 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import traceback
 | 
					import traceback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,7 +49,9 @@ if __name__ == "__main__":
 | 
				
			|||||||
    logging.root.setLevel(LOG_LEVELS[args.log_level])
 | 
					    logging.root.setLevel(LOG_LEVELS[args.log_level])
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        os.makedirs(args.destination)
 | 
					        if not os.path.isdir(args.destination):
 | 
				
			||||||
 | 
					            os.makedirs(args.destination)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not args.password:
 | 
					        if not args.password:
 | 
				
			||||||
            args.password = getpass.getpass("Enter password: ")
 | 
					            args.password = getpass.getpass("Enter password: ")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@ -61,39 +59,10 @@ if __name__ == "__main__":
 | 
				
			|||||||
            log.info("fetching activities for {} ...".format(args.username))
 | 
					            log.info("fetching activities for {} ...".format(args.username))
 | 
				
			||||||
            activity_ids = client.list_activity_ids()
 | 
					            activity_ids = client.list_activity_ids()
 | 
				
			||||||
            for index, id in enumerate(activity_ids):
 | 
					            for index, id in enumerate(activity_ids):
 | 
				
			||||||
                log.info("processing activity {} out of {} ...".format(
 | 
					                log.info("processing activity {} ({} out of {}) ...".format(
 | 
				
			||||||
                    index+1, len(activity_ids)))
 | 
					                    id, index+1, len(activity_ids)))
 | 
				
			||||||
                activity_summary = client.get_activity_summary(id)
 | 
					                garminexport.util.save_activity(
 | 
				
			||||||
                activity_details = client.get_activity_details(id)
 | 
					                    client, id, args.destination)
 | 
				
			||||||
                activity_gpx = client.get_activity_gpx(id)
 | 
					 | 
				
			||||||
                activity_tcx = client.get_activity_tcx(id)
 | 
					 | 
				
			||||||
                activity_fit = client.get_activity_fit(id)
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                # for each activitity save the summary, details and GPX file.
 | 
					 | 
				
			||||||
                creation_millis = activity_summary["activity"]["uploadDate"]["millis"]
 | 
					 | 
				
			||||||
                timestamp = datetime.fromtimestamp(int(creation_millis)/1000.0)
 | 
					 | 
				
			||||||
                filename_prefix = "{}_{}".format(
 | 
					 | 
				
			||||||
                    timestamp.strftime("%Y%m%d-%H%M%S"), id)
 | 
					 | 
				
			||||||
                path_prefix = os.path.join(args.destination, filename_prefix)
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                summary_file = path_prefix + "_summary.json"
 | 
					 | 
				
			||||||
                details_file = path_prefix + "_details.json"
 | 
					 | 
				
			||||||
                gpx_file = path_prefix + ".gpx"
 | 
					 | 
				
			||||||
                tcx_file = path_prefix + ".tcx"
 | 
					 | 
				
			||||||
                fit_file = path_prefix + ".fit"
 | 
					 | 
				
			||||||
                with codecs.open(summary_file, encoding="utf-8", mode="w") as f:
 | 
					 | 
				
			||||||
                    f.write(json.dumps(
 | 
					 | 
				
			||||||
                        activity_summary, ensure_ascii=False, indent=4))
 | 
					 | 
				
			||||||
                with codecs.open(details_file, encoding="utf-8", mode="w") as f:
 | 
					 | 
				
			||||||
                    f.write(json.dumps(
 | 
					 | 
				
			||||||
                        activity_details, ensure_ascii=False, indent=4))
 | 
					 | 
				
			||||||
                with codecs.open(gpx_file, encoding="utf-8", mode="w") as f:
 | 
					 | 
				
			||||||
                    f.write(activity_gpx)
 | 
					 | 
				
			||||||
                with codecs.open(tcx_file, encoding="utf-8", mode="w") as f:
 | 
					 | 
				
			||||||
                    f.write(activity_tcx)
 | 
					 | 
				
			||||||
                if activity_fit:
 | 
					 | 
				
			||||||
                    with open(fit_file, mode="wb") as f:
 | 
					 | 
				
			||||||
                        f.write(activity_fit)
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        exc_type, exc_value, exc_traceback = sys.exc_info()
 | 
					        exc_type, exc_value, exc_traceback = sys.exc_info()
 | 
				
			||||||
        log.error(u"failed with exception: %s", e)
 | 
					        log.error(u"failed with exception: %s", e)
 | 
				
			||||||
 | 
				
			|||||||
@ -180,7 +180,8 @@ class GarminClient(object):
 | 
				
			|||||||
        :returns: A list of activity identifiers.
 | 
					        :returns: A list of activity identifiers.
 | 
				
			||||||
        :rtype: list of str
 | 
					        :rtype: list of str
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        log.info("fetching activities {} through {} ...".format(start_index, start_index+max_limit-1))
 | 
					        log.debug("fetching activities {} through {} ...".format(
 | 
				
			||||||
 | 
					            start_index, start_index+max_limit-1))
 | 
				
			||||||
        response = self.session.get(
 | 
					        response = self.session.get(
 | 
				
			||||||
            "https://connect.garmin.com/proxy/activity-search-service-1.2/json/activities", params={"start": start_index, "limit": max_limit})
 | 
					            "https://connect.garmin.com/proxy/activity-search-service-1.2/json/activities", params={"start": start_index, "limit": max_limit})
 | 
				
			||||||
        if response.status_code != 200:
 | 
					        if response.status_code != 200:
 | 
				
			||||||
@ -292,5 +293,5 @@ class GarminClient(object):
 | 
				
			|||||||
        # fit file returned from server is in a zip archive
 | 
					        # fit file returned from server is in a zip archive
 | 
				
			||||||
        zipped_fit_file = response.content
 | 
					        zipped_fit_file = response.content
 | 
				
			||||||
        zip = zipfile.ZipFile(StringIO(zipped_fit_file), mode="r")
 | 
					        zip = zipfile.ZipFile(StringIO(zipped_fit_file), mode="r")
 | 
				
			||||||
        # return the "<activity-id>.fit" entry from the zip archive
 | 
					        # return the "<activity-activity_id>.fit" entry from the zip archive
 | 
				
			||||||
        return zip.open(str(activity_id) + ".fit").read()
 | 
					        return zip.open(str(activity_id) + ".fit").read()
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										53
									
								
								garminexport/util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								garminexport/util.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					#! /usr/bin/env python
 | 
				
			||||||
 | 
					"""A module with utility functions."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import codecs
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def save_activity(client, activity_id, destination):
 | 
				
			||||||
 | 
					    """Downloads a certain Garmin Connect activity and saves it
 | 
				
			||||||
 | 
					    to a given destination directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param client: A :class:`garminexport.garminclient.GarminClient`
 | 
				
			||||||
 | 
					      instance that is assumed to be connected.
 | 
				
			||||||
 | 
					    :type client: :class:`garminexport.garminclient.GarminClient`
 | 
				
			||||||
 | 
					    :param activity_id: Activity identifier.
 | 
				
			||||||
 | 
					    :type activity_id: int
 | 
				
			||||||
 | 
					    :param destination: Destination directory (assumed to exist already).
 | 
				
			||||||
 | 
					    :type destination: str
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    activity_summary = client.get_activity_summary(activity_id)
 | 
				
			||||||
 | 
					    activity_details = client.get_activity_details(activity_id)
 | 
				
			||||||
 | 
					    activity_gpx = client.get_activity_gpx(activity_id)
 | 
				
			||||||
 | 
					    activity_tcx = client.get_activity_tcx(activity_id)
 | 
				
			||||||
 | 
					    activity_fit = client.get_activity_fit(activity_id)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					    # save activitity summary, details and GPX, TCX and FIT file.
 | 
				
			||||||
 | 
					    creation_millis = activity_summary["activity"]["uploadDate"]["millis"]
 | 
				
			||||||
 | 
					    timestamp = datetime.fromtimestamp(int(creation_millis)/1000.0)
 | 
				
			||||||
 | 
					    filename_prefix = "{}_{}".format(
 | 
				
			||||||
 | 
					        timestamp.strftime("%Y%m%d-%H%M%S"), activity_id)
 | 
				
			||||||
 | 
					    path_prefix = os.path.join(destination, filename_prefix)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    summary_file = path_prefix + "_summary.json"
 | 
				
			||||||
 | 
					    details_file = path_prefix + "_details.json"
 | 
				
			||||||
 | 
					    gpx_file = path_prefix + ".gpx"
 | 
				
			||||||
 | 
					    tcx_file = path_prefix + ".tcx"
 | 
				
			||||||
 | 
					    fit_file = path_prefix + ".fit"
 | 
				
			||||||
 | 
					    with codecs.open(summary_file, encoding="utf-8", mode="w") as f:
 | 
				
			||||||
 | 
					        f.write(json.dumps(
 | 
				
			||||||
 | 
					            activity_summary, ensure_ascii=False, indent=4))
 | 
				
			||||||
 | 
					    with codecs.open(details_file, encoding="utf-8", mode="w") as f:
 | 
				
			||||||
 | 
					        f.write(json.dumps(
 | 
				
			||||||
 | 
					            activity_details, ensure_ascii=False, indent=4))
 | 
				
			||||||
 | 
					    with codecs.open(gpx_file, encoding="utf-8", mode="w") as f:
 | 
				
			||||||
 | 
					        f.write(activity_gpx)
 | 
				
			||||||
 | 
					    with codecs.open(tcx_file, encoding="utf-8", mode="w") as f:
 | 
				
			||||||
 | 
					        f.write(activity_tcx)
 | 
				
			||||||
 | 
					    if activity_fit:
 | 
				
			||||||
 | 
					        with open(fit_file, mode="wb") as f:
 | 
				
			||||||
 | 
					            f.write(activity_fit)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
							
								
								
									
										108
									
								
								incremental_backup.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										108
									
								
								incremental_backup.py
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					#! /usr/bin/env python
 | 
				
			||||||
 | 
					"""Performs (incremental) backups of activities for a given Garmin Connect
 | 
				
			||||||
 | 
					account.
 | 
				
			||||||
 | 
					The activities are stored in a local directory on the user's computer.
 | 
				
			||||||
 | 
					The backups are incremental, meaning that only activities that aren't already
 | 
				
			||||||
 | 
					stored in the backup directory will be downloaded.
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					import getpass
 | 
				
			||||||
 | 
					from garminexport.garminclient import GarminClient
 | 
				
			||||||
 | 
					import garminexport.util
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import traceback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logging.basicConfig(
 | 
				
			||||||
 | 
					    level=logging.INFO, format="%(asctime)-15s [%(levelname)s] %(message)s")
 | 
				
			||||||
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOG_LEVELS = {
 | 
				
			||||||
 | 
					    "DEBUG": logging.DEBUG,
 | 
				
			||||||
 | 
					    "INFO": logging.INFO,
 | 
				
			||||||
 | 
					    "WARNING": logging.WARNING,
 | 
				
			||||||
 | 
					    "ERROR": logging.ERROR
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					"""Command-line (string-based) log-level mapping to logging module levels."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_backed_up_ids(backup_dir):
 | 
				
			||||||
 | 
					    """Return all activitiy ids that have been backed up in the
 | 
				
			||||||
 | 
					    given backup directory.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :rtype: list of int
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    # backed up activities follow this pattern
 | 
				
			||||||
 | 
					    activity_file_pattern = r'[0-9]+\-[0-9]+_([0-9]+)_summary\.json'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    backed_up_ids = []
 | 
				
			||||||
 | 
					    dir_entries = os.listdir(backup_dir)
 | 
				
			||||||
 | 
					    for entry in dir_entries:
 | 
				
			||||||
 | 
					        activity_match = re.search(activity_file_pattern, entry)
 | 
				
			||||||
 | 
					        if activity_match:
 | 
				
			||||||
 | 
					            backed_up_id = int(activity_match.group(1))
 | 
				
			||||||
 | 
					            backed_up_ids.append(backed_up_id)
 | 
				
			||||||
 | 
					    return backed_up_ids           
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    parser = argparse.ArgumentParser(
 | 
				
			||||||
 | 
					        description=(
 | 
				
			||||||
 | 
					            "Performs incremental backups of activities for a "
 | 
				
			||||||
 | 
					            "given Garmin Connect account. Only activities that "
 | 
				
			||||||
 | 
					            "aren't already stored in the backup directory will "
 | 
				
			||||||
 | 
					            "be downloaded."))
 | 
				
			||||||
 | 
					    # positional args
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "username", metavar="<username>", type=str, help="Account user name.")
 | 
				
			||||||
 | 
					    # optional args
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--password", type=str, help="Account password.")
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--backup-dir", metavar="DIR", type=str,
 | 
				
			||||||
 | 
					        help=("Destination directory for downloaded activities. Default: "
 | 
				
			||||||
 | 
					              "./activities/"), default=os.path.join(".", "activities"))
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--log-level", metavar="LEVEL", type=str,
 | 
				
			||||||
 | 
					        help=("Desired log output level (DEBUG, INFO, WARNING, ERROR). "
 | 
				
			||||||
 | 
					              "Default: INFO."), default="INFO")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    args = parser.parse_args()
 | 
				
			||||||
 | 
					    if not args.log_level in LOG_LEVELS:
 | 
				
			||||||
 | 
					        raise ValueError("Illegal log-level argument: {}".format(args.log_level))
 | 
				
			||||||
 | 
					    logging.root.setLevel(LOG_LEVELS[args.log_level])
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        if not os.path.isdir(args.backup_dir):
 | 
				
			||||||
 | 
					            os.makedirs(args.backup_dir)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        if not args.password:
 | 
				
			||||||
 | 
					            args.password = getpass.getpass("Enter password: ")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        with GarminClient(args.username, args.password) as client:
 | 
				
			||||||
 | 
					            # already backed up activities (stored in backup-dir)
 | 
				
			||||||
 | 
					            backed_up_activities = set(get_backed_up_ids(args.backup_dir))
 | 
				
			||||||
 | 
					            log.info("{} contains {} backed up activities.".format(
 | 
				
			||||||
 | 
					                args.backup_dir, len(backed_up_activities)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # get all activity ids from Garmin account
 | 
				
			||||||
 | 
					            log.info("retrieving activities for {} ...".format(args.username))
 | 
				
			||||||
 | 
					            all_activities = set(client.list_activity_ids())
 | 
				
			||||||
 | 
					            log.info("account has a total of {} activities.".format(
 | 
				
			||||||
 | 
					                len(all_activities)))
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            missing_activities = all_activities - backed_up_activities
 | 
				
			||||||
 | 
					            log.info("activities that haven't been backed up: {}".format(
 | 
				
			||||||
 | 
					                len(missing_activities)))
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for index, id in enumerate(missing_activities):
 | 
				
			||||||
 | 
					                log.info("backing up activity {} ({} out of {}) ...".format(
 | 
				
			||||||
 | 
					                    id, index+1, len(missing_activities)))
 | 
				
			||||||
 | 
					                garminexport.util.save_activity(
 | 
				
			||||||
 | 
					                    client, id, args.backup_dir)
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        exc_type, exc_value, exc_traceback = sys.exc_info()
 | 
				
			||||||
 | 
					        log.error(u"failed with exception: %s", e)
 | 
				
			||||||
 | 
					        raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user