def test_initial_delay(self): """The initial delay is used to scale the series of delays.""" self.strategy = ExponentialBackoffDelayStrategy(timedelta(seconds=2)) self.assertEqual(self.strategy.next_delay(0), timedelta(seconds=0)) self.assertEqual(self.strategy.next_delay(1), timedelta(seconds=2 * 1)) self.assertEqual(self.strategy.next_delay(2), timedelta(seconds=2 * 2)) self.assertEqual(self.strategy.next_delay(3), timedelta(seconds=2 * 4)) self.assertEqual(self.strategy.next_delay(4), timedelta(seconds=2 * 8)) self.assertEqual(self.strategy.next_delay(5), timedelta(seconds=2 * 16)) self.assertEqual(self.strategy.next_delay(10), timedelta(seconds=2 * 512))
class TestExponentialBackoffDelayStrategy(unittest.TestCase): """Exercise `ExponentialBackoffDelayStrategy`.""" def setUp(self): # object under test self.strategy = ExponentialBackoffDelayStrategy(timedelta(seconds=1)) def test_calculate_delay(self): """`ExponentialBackoffDelayStrategy` should return exponentially increasing delay.""" self.assertEqual(self.strategy.next_delay(0), timedelta(seconds=0)) self.assertEqual(self.strategy.next_delay(1), timedelta(seconds=1)) self.assertEqual(self.strategy.next_delay(2), timedelta(seconds=2)) self.assertEqual(self.strategy.next_delay(3), timedelta(seconds=4)) self.assertEqual(self.strategy.next_delay(4), timedelta(seconds=8)) self.assertEqual(self.strategy.next_delay(5), timedelta(seconds=16)) self.assertEqual(self.strategy.next_delay(10), timedelta(seconds=512)) def test_initial_delay(self): """The initial delay is used to scale the series of delays.""" self.strategy = ExponentialBackoffDelayStrategy(timedelta(seconds=2)) self.assertEqual(self.strategy.next_delay(0), timedelta(seconds=0)) self.assertEqual(self.strategy.next_delay(1), timedelta(seconds=2 * 1)) self.assertEqual(self.strategy.next_delay(2), timedelta(seconds=2 * 2)) self.assertEqual(self.strategy.next_delay(3), timedelta(seconds=2 * 4)) self.assertEqual(self.strategy.next_delay(4), timedelta(seconds=2 * 8)) self.assertEqual(self.strategy.next_delay(5), timedelta(seconds=2 * 16)) self.assertEqual(self.strategy.next_delay(10), timedelta(seconds=2 * 512))
class TestExponentialBackoffDelayStrategy(unittest.TestCase): """Exercise `ExponentialBackoffDelayStrategy`.""" def setUp(self): # object under test self.strategy = ExponentialBackoffDelayStrategy(timedelta(seconds=1)) def test_calculate_delay(self): """`ExponentialBackoffDelayStrategy` should return exponentially increasing delay.""" self.assertEqual(self.strategy.next_delay(0), timedelta(seconds=0)) self.assertEqual(self.strategy.next_delay(1), timedelta(seconds=1)) self.assertEqual(self.strategy.next_delay(2), timedelta(seconds=2)) self.assertEqual(self.strategy.next_delay(3), timedelta(seconds=4)) self.assertEqual(self.strategy.next_delay(4), timedelta(seconds=8)) self.assertEqual(self.strategy.next_delay(5), timedelta(seconds=16)) self.assertEqual(self.strategy.next_delay(10), timedelta(seconds=512)) def test_initial_delay(self): """The initial delay is used to scale the series of delays.""" self.strategy = ExponentialBackoffDelayStrategy(timedelta(seconds=2)) self.assertEqual(self.strategy.next_delay(0), timedelta(seconds=0)) self.assertEqual(self.strategy.next_delay(1), timedelta(seconds=2*1)) self.assertEqual(self.strategy.next_delay(2), timedelta(seconds=2*2)) self.assertEqual(self.strategy.next_delay(3), timedelta(seconds=2*4)) self.assertEqual(self.strategy.next_delay(4), timedelta(seconds=2*8)) self.assertEqual(self.strategy.next_delay(5), timedelta(seconds=2*16)) self.assertEqual(self.strategy.next_delay(10), timedelta(seconds=2*512))
def gcff(config_file): config = { "username": "", "password": "", "backup_dir": os.path.join(".", "gc_backup"), "export_formats": "fit", "ignore_errors": False, "max_retries": 3, "download_from_date": "1900-01-01 00:00:00" } config_file = os.path.expanduser(config_file) if not os.path.isfile(config_file): print("Configuration file not found: " + config_file) return with open(config_file) as f: try: user_config = json.load(f) except json.decoder.JSONDecodeError as err: print("Configuration file error: " + str(err)) return config.update(user_config) if not os.path.isdir(config["backup_dir"]): os.makedirs(config["backup_dir"]) rr = Retryer(delay_strategy=ExponentialBackoffDelayStrategy( initial_delay=timedelta(seconds=1)), stop_strategy=MaxRetriesStopStrategy(config["max_retries"])) with GarminClient(config["username"], config["password"]) as gc: # get all activity ids and timestamps print("Scanning activities for {}".format(config["username"])) activities = rr.call(gc.list_activities) print("Total activities: {}".format(len(activities))) # print(activities) from_date_str = config["download_from_date"] new_activities = get_activities_since(activities, from_date_str) print("Activities to backup[since: {}]: {}".format( from_date_str, len(new_activities))) last_date = None for i, activity in enumerate(new_activities): _id, activity_date = activity print("Downloading[{}]: {} - {}".format(i, _id, activity_date)) try: garminexport.backup.download(gc, activity, rr, config["backup_dir"], config["export_formats"]) except Exception as err: print("failed with exception: {}".format(err)) if not config["ignore_errors"]: raise if last_date is None or last_date < activity_date: last_date = activity_date store_last_activity_date_in_config_file(config_file, config, last_date)
def incremental_backup(username: str, password: str = None, backup_dir: str = os.path.join(".", "activities"), export_formats: str = 'ALL', ignore_errors: bool = False, max_retries: int = 7): """Performs (incremental) backups of activities for a given Garmin Connect account. :param username: Garmin Connect user name :param password: Garmin Connect user password. Default: None. If not provided, would be asked interactively. :param backup_dir: Destination directory for downloaded activities. Default: ./activities/". :param export_formats: Desired output formats (json_summary, json_details, gpx, kml, tcx, fit). Default: ALL. :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. Exponential backoff will be used, meaning that the delay between successive attempts will double with every retry, starting at one second. Default: 7. 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. """ # if no --format was specified, all formats are to be backed up export_formats = export_formats if export_formats else supported_export_formats log.info("backing up formats: %s", ", ".join(export_formats)) if not os.path.isdir(backup_dir): os.makedirs(backup_dir) if not password: password = getpass.getpass("Enter password: "******"scanning activities for %s ...", username) activities = set(retryer.call(client.list_activities)) log.info("account has a total of %d activities", len(activities)) missing_activities = garminexport.backup.need_backup(activities, backup_dir, export_formats) backed_up = activities - missing_activities log.info("%s contains %d backed up activities", backup_dir, len(backed_up)) log.info("activities that aren't backed up: %d", len(missing_activities)) for index, activity in enumerate(missing_activities): id, start = activity log.info("backing up activity %s from %s (%d out of %d) ...", id, start, index + 1, len(missing_activities)) try: garminexport.backup.download(client, activity, retryer, backup_dir, export_formats) except Exception as e: log.error("failed with exception: %s", e) if not ignore_errors: raise
def __init__(self, username, password, initial_delay=timedelta(seconds=1), max_retries=3): self._client = GarminClient(username, password) self._retryer = Retryer( delay_strategy=ExponentialBackoffDelayStrategy( initial_delay=initial_delay), stop_strategy=MaxRetriesStopStrategy(max_retries))
def test_initial_delay(self): """The initial delay is used to scale the series of delays.""" self.strategy = ExponentialBackoffDelayStrategy(timedelta(seconds=2)) self.assertEqual(self.strategy.next_delay(0), timedelta(seconds=0)) self.assertEqual(self.strategy.next_delay(1), timedelta(seconds=2*1)) self.assertEqual(self.strategy.next_delay(2), timedelta(seconds=2*2)) self.assertEqual(self.strategy.next_delay(3), timedelta(seconds=2*4)) self.assertEqual(self.strategy.next_delay(4), timedelta(seconds=2*8)) self.assertEqual(self.strategy.next_delay(5), timedelta(seconds=2*16)) self.assertEqual(self.strategy.next_delay(10), timedelta(seconds=2*512))
formats = formats if formats else export_formats log.info("backing up formats: %s", ", ".join(formats)) logging.root.setLevel(LOG_LEVELS[log_level]) try: if not os.path.isdir(backup_dir): os.makedirs(backup_dir) if not password: raise ValueError("Password must be provided") # set up a retryer that will handle retries of failed activity # downloads retryer = Retryer( delay_strategy=ExponentialBackoffDelayStrategy( initial_delay=timedelta(seconds=1)), stop_strategy=MaxRetriesStopStrategy(DEFAULT_MAX_RETRIES)) with GarminClient(username, password) as client: # get all activity ids and timestamps from Garmin account log.info("scanning activities for %s ...", username) activities = set(retryer.call(client.list_activities)) log.info("account has a total of %d activities", len(activities)) missing_activities = garminexport.backup.need_backup( activities, backup_dir, formats) backed_up = activities - missing_activities log.info("%s contains %d backed up activities", backup_dir, len(backed_up)) log.info("activities that aren't backed up: %d",
def main(): parser = argparse.ArgumentParser( description= "Downloads one particular activity for a given Garmin Connect account." ) # positional args parser.add_argument("username", metavar="<username>", type=str, help="Account user name.") parser.add_argument("activity", metavar="<activity>", type=int, help="Activity ID.") parser.add_argument("format", metavar="<format>", type=str, help="Export format (one of: {}).".format( garminexport.backup.supported_export_formats)) # optional args parser.add_argument("--password", type=str, help="Account password.") parser.add_argument( "--destination", metavar="DIR", type=str, help= "Destination directory for downloaded activity. 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 args.log_level not in LOG_LEVELS: raise ValueError("Illegal log-level argument: {}".format( args.log_level)) if args.format not in garminexport.backup.supported_export_formats: raise ValueError( "Unrecognized export format: '{}'. Must be one of {}".format( args.format, garminexport.backup.supported_export_formats)) logging.root.setLevel(LOG_LEVELS[args.log_level]) try: if not os.path.isdir(args.destination): os.makedirs(args.destination) if not args.password: args.password = getpass.getpass("Enter password: "******"fetching activity %s ...", args.activity) summary = client.get_activity_summary(args.activity) # set up a retryer that will handle retries of failed activity downloads retryer = Retryer(delay_strategy=ExponentialBackoffDelayStrategy( initial_delay=timedelta(seconds=1)), stop_strategy=MaxRetriesStopStrategy(5)) start_time = dateutil.parser.parse( summary["summaryDTO"]["startTimeGMT"]) garminexport.backup.download(client, (args.activity, start_time), retryer, args.destination, export_formats=[args.format]) except Exception as e: log.error("failed with exception: %s", e) raise
def upload_activity(self, file, format=None, name=None, description=None, activity_type=None, private=None): """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` :type format: str :param name: Optional name for the activity on Garmin Connect :type name: str :param description: Optional description for the activity on Garmin Connect :type description: str :param activity_type: Optional activityType key (lowercase: e.g. running, cycling) :type activityType: str :param private: If true, then activity will be set as private. :type private: bool :returns: ID of the newly-uploaded activity :rtype: int """ if isinstance(file, str): file = open(file, "rb") # guess file type if unspecified fn = os.path.basename(file.name) _, ext = os.path.splitext(fn) if format is None: if ext.lower() in ('.gpx', '.tcx', '.fit'): format = ext.lower()[1:] else: raise Exception(u"could not guess file type for {}".format(fn)) # upload it files = dict(data=(fn, file)) response = self.session.post( "https://connect.garmin.com/modern/proxy/upload-service/upload/.{}" .format(format), files=files, headers={"nk": "NT"}) # check response and get activity ID try: j = response.json()["detailedImportResult"] except (json.JSONDecodeError, KeyError): raise Exception(u"failed to upload {} for activity: {}\n{}".format( format, response.status_code, response.text)) # single activity, immediate success if len(j["successes"]) == 1 and len(j["failures"]) == 0: activity_id = j["successes"][0]["internalId"] # duplicate of existing activity elif len(j["failures"]) == 1 and len( j["successes"]) == 0 and response.status_code == 409: log.info(u"duplicate activity uploaded, continuing") activity_id = j["failures"][0]["internalId"] # need to poll until success/failure elif len(j["failures"]) == 0 and len( j["successes"]) == 0 and response.status_code == 202: retryer = Retryer( returnval_predicate=bool, delay_strategy=ExponentialBackoffDelayStrategy( initial_delay=timedelta(seconds=1)), stop_strategy=MaxRetriesStopStrategy( 6), # wait for up to 64 seconds (2**6) error_strategy=None) activity_id = retryer.call(self._poll_upload_completion, j["uploadUuid"]["uuid"], j["creationDate"]) # don't know how to handle multiple activities elif len(j["successes"]) > 1: raise Exception( u"uploading {} resulted in multiple activities ({})".format( format, len(j["successes"]))) # all other errors else: raise Exception(u"failed to upload {} for activity: {}\n{}".format( format, response.status_code, j["failures"])) # add optional fields data = {} if name is not None: data['activityName'] = name if description is not None: data['description'] = description if activity_type is not None: data['activityTypeDTO'] = {"typeKey": activity_type} if private: data['privacy'] = {"typeKey": "private"} if data: data['activityId'] = activity_id 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 set metadata for activity {}: {}\n{}".format( activity_id, response.status_code, response.text)) return activity_id
def setUp(self): # object under test self.strategy = ExponentialBackoffDelayStrategy(timedelta(seconds=1))
def incremental_backup(username: str, password: str = None, user_agent_fn: Callable[[],str] = None, backup_dir: str = os.path.join(".", "activities"), export_formats: List[str] = None, ignore_errors: bool = False, max_retries: int = 7): """Performs (incremental) backups of activities for a given Garmin Connect account. :param username: Garmin Connect user name :param password: Garmin Connect user password. Default: None. If not provided, would be asked interactively. :keyword user_agent_fn: A function that, when called, produces a `User-Agent` string to be used as `User-Agent` for the remainder of the session. If set to None, the default user agent of the http request 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). 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. Exponential backoff will be used, meaning that the delay between successive attempts will double with every retry, starting at one second. Default: 7. 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. """ # if no --format was specified, all formats are to be backed up export_formats = export_formats if export_formats else supported_export_formats log.info("backing up formats: %s", ", ".join(export_formats)) if not os.path.isdir(backup_dir): os.makedirs(backup_dir) if not password: password = getpass.getpass("Enter password: "******"scanning activities for %s ...", username) activities = set(retryer.call(client.list_activities)) log.info("account has a total of %d activities", len(activities)) missing_activities = garminexport.backup.need_backup(activities, backup_dir, export_formats) backed_up = activities - missing_activities log.info("%s contains %d backed up activities", backup_dir, len(backed_up)) log.info("activities that aren't backed up: %d", len(missing_activities)) for index, activity in enumerate(missing_activities): id, start = activity if id not in [578518094]: log.info("backing up activity %s from %s (%d out of %d) ...", id, start, index + 1, len(missing_activities)) try: garminexport.backup.download(client, activity, retryer, backup_dir, export_formats) except Exception as e: log.error("failed with exception: %s", e) if not ignore_errors: raise # gewicht herunterladen import urllib.request, json import datetime import time WeightURL = "https://connect.garmin.com/modern/proxy/userprofile-service/userprofile/personal-information/weightWithOutbound/filterByDay?from="+str(int(datetime.datetime(2008,1,1,0,0).timestamp()))+"999"+"&until="+str(int(time.time()))+"999" response = client.session.get(WeightURL) if response.status_code in (404, 204): log.info("failed to download weight 404,204") if response.status_code != 200: log.info("failed to download weight ungleich 200") else: data = json.loads(response.text) heute = datetime.date.today() with open(os.path.join(backup_dir,'weight_'+str(heute.year)+'-'+str(heute.month)+'-'+str(heute.day)+'.json'), 'w') as outfile: json.dump(data,outfile)