first commit
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 3m2s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 3m2s
This commit is contained in:
123
fitbitsync.py
123
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user