first commit
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 3m2s

This commit is contained in:
2025-12-14 13:32:47 -08:00
parent 950580a80f
commit 5ac0a84953

View File

@@ -1,6 +1,7 @@
# Fitbit to Garmin Weight Sync Application # Fitbit to Garmin Weight Sync Application
# Syncs weight data from Fitbit API to Garmin Connect # Syncs weight data from Fitbit API to Garmin Connect
import sys
import asyncio import asyncio
import json import json
import logging import logging
@@ -72,6 +73,110 @@ class ConfigManager:
self.config_file = Path(config_file) self.config_file = Path(config_file)
self.config_file.parent.mkdir(parents=True, exist_ok=True) self.config_file.parent.mkdir(parents=True, exist_ok=True)
self.config = self._load_config() 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: def _load_config(self) -> Dict:
"""Load configuration from file""" """Load configuration from file"""
@@ -444,6 +549,12 @@ class FitbitClient:
def _setup_credentials(self) -> bool: def _setup_credentials(self) -> bool:
"""Setup Fitbit credentials interactively""" """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("\n🔑 Fitbit API Credentials Setup")
print("=" * 40) print("=" * 40)
print("To get your Fitbit API credentials:") 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: async def _oauth_flow(self, client_id: str, client_secret: str) -> bool:
"""Perform OAuth 2.0 authorization flow""" """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: try:
redirect_uri = self.config.get('fitbit.redirect_uri') redirect_uri = self.config.get('fitbit.redirect_uri')
@@ -714,6 +831,12 @@ class GarminClient:
def _setup_credentials(self) -> bool: def _setup_credentials(self) -> bool:
"""Setup Garmin credentials interactively""" """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("\n🔑 Garmin Connect Credentials Setup")
print("=" * 40) print("=" * 40)