From 5ac0a84953dc226d5af1468ed22feb0535c142a1 Mon Sep 17 00:00:00 2001 From: sstent Date: Sun, 14 Dec 2025 13:32:47 -0800 Subject: [PATCH] first commit --- fitbitsync.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/fitbitsync.py b/fitbitsync.py index a5ee8c5..84fb4da 100644 --- a/fitbitsync.py +++ b/fitbitsync.py @@ -1,6 +1,7 @@ # Fitbit to Garmin Weight Sync Application # Syncs weight data from Fitbit API to Garmin Connect +import sys import asyncio import json import logging @@ -72,6 +73,110 @@ class ConfigManager: self.config_file = Path(config_file) self.config_file.parent.mkdir(parents=True, exist_ok=True) self.config = self._load_config() + self._load_from_environment() # Load from env vars first + + if os.getenv('CONFIG_SOURCE') == 'consul': + self._load_from_consul() + + def _load_from_environment(self): + """Override config with environment variables.""" + logger.info("Checking for environment variable configuration...") + + # Fitbit + if 'FITBIT_CLIENT_ID' in os.environ: + self.config['fitbit']['client_id'] = os.environ['FITBIT_CLIENT_ID'] + logger.info("Loaded FITBIT_CLIENT_ID from environment.") + if 'FITBIT_CLIENT_SECRET' in os.environ: + self.config['fitbit']['client_secret'] = os.environ['FITBIT_CLIENT_SECRET'] + logger.info("Loaded FITBIT_CLIENT_SECRET from environment.") + if 'FITBIT_ACCESS_TOKEN' in os.environ: + self.config['fitbit']['access_token'] = os.environ['FITBIT_ACCESS_TOKEN'] + logger.info("Loaded FITBIT_ACCESS_TOKEN from environment.") + if 'FITBIT_REFRESH_TOKEN' in os.environ: + self.config['fitbit']['refresh_token'] = os.environ['FITBIT_REFRESH_TOKEN'] + logger.info("Loaded FITBIT_REFRESH_TOKEN from environment.") + + # Garmin + if 'GARMIN_USERNAME' in os.environ: + self.config['garmin']['username'] = os.environ['GARMIN_USERNAME'] + logger.info("Loaded GARMIN_USERNAME from environment.") + if 'GARMIN_PASSWORD' in os.environ: + self.config['garmin']['password'] = os.environ['GARMIN_PASSWORD'] + logger.info("Loaded GARMIN_PASSWORD from environment.") + + # Consul + if 'CONSUL_HOST' in os.environ: + self.config['consul']['host'] = os.environ['CONSUL_HOST'] + logger.info("Loaded CONSUL_HOST from environment.") + if 'CONSUL_PORT' in os.environ: + try: + self.config['consul']['port'] = int(os.environ['CONSUL_PORT']) + logger.info("Loaded CONSUL_PORT from environment.") + except ValueError: + logger.error("Invalid CONSUL_PORT in environment. Must be an integer.") + + def _deep_merge(self, base: Dict, new: Dict): + """ + Deep merge 'new' dict into 'base' dict. Overwrites values in base. + """ + for key, value in new.items(): + if isinstance(value, dict) and key in base and isinstance(base[key], dict): + self._deep_merge(base[key], value) + else: + base[key] = value + + def _load_from_consul(self): + """Load configuration from Consul, overwriting existing values.""" + if not CONSUL_LIBRARY: + logger.warning("Consul library not installed, cannot load config from Consul.") + return + + logger.info("Attempting to load configuration from Consul...") + consul_config = self.get('consul') + try: + c = consul.Consul( + host=consul_config.get('host', 'localhost'), + port=consul_config.get('port', 8500) + ) + prefix = consul_config.get('prefix', 'fitbit-garmin-sync').strip('/') + config_prefix = f"{prefix}/config/" + + index, data = c.kv.get(config_prefix, recurse=True) + + if not data: + logger.info("No configuration found in Consul at prefix: %s", config_prefix) + return + + consul_conf = {} + for item in data: + key_path = item['Key'].replace(config_prefix, '').split('/') + value_str = item['Value'].decode('utf-8') + + # Try to convert value to appropriate type + value: object + if value_str.lower() == 'true': + value = True + elif value_str.lower() == 'false': + value = False + elif value_str.isdigit(): + value = int(value_str) + else: + try: + value = float(value_str) + except ValueError: + value = value_str # It's a string + + temp_conf = consul_conf + for part in key_path[:-1]: + temp_conf = temp_conf.setdefault(part, {}) + temp_conf[key_path[-1]] = value + + # Deep merge consul_conf into self.config + self._deep_merge(self.config, consul_conf) + logger.info("Successfully loaded and merged configuration from Consul.") + + except Exception as e: + logger.error("Failed to load configuration from Consul: %s", e) def _load_config(self) -> Dict: """Load configuration from file""" @@ -444,6 +549,12 @@ class FitbitClient: def _setup_credentials(self) -> bool: """Setup Fitbit credentials interactively""" + import sys + if not sys.stdout.isatty(): + logger.error("Running in a non-interactive environment. Cannot prompt for credentials.") + logger.error("Please set credentials using environment variables (e.g., FITBIT_CLIENT_ID) or Consul.") + return False + print("\nšŸ”‘ Fitbit API Credentials Setup") print("=" * 40) print("To get your Fitbit API credentials:") @@ -473,6 +584,12 @@ class FitbitClient: async def _oauth_flow(self, client_id: str, client_secret: str) -> bool: """Perform OAuth 2.0 authorization flow""" + import sys + if not sys.stdout.isatty(): + logger.error("Cannot perform OAuth flow in a non-interactive environment.") + logger.error("Please provide FITBIT_ACCESS_TOKEN and FITBIT_REFRESH_TOKEN via environment variables or Consul.") + return False + try: redirect_uri = self.config.get('fitbit.redirect_uri') @@ -714,6 +831,12 @@ class GarminClient: def _setup_credentials(self) -> bool: """Setup Garmin credentials interactively""" + import sys + if not sys.stdout.isatty(): + logger.error("Running in a non-interactive environment. Cannot prompt for credentials.") + logger.error("Please set credentials using environment variables (e.g., GARMIN_USERNAME, GARMIN_PASSWORD).") + return False + print("\nšŸ”‘ Garmin Connect Credentials Setup") print("=" * 40)