#!/usr/bin/env python3 """ Configuration Management Loads configuration from YAML file or environment variables. """ import os import yaml import logging from pathlib import Path from typing import Dict, Optional, Any try: from dotenv import load_dotenv DOTENV_AVAILABLE = True except ImportError: DOTENV_AVAILABLE = False DEFAULT_CONFIG = { 'sharepoint': { 'enabled': False, 'site_url': '', 'folder_path': '/Shared Documents/Reports', 'file_path': None, # Use folder_path for multiple files, file_path for single file 'local_dir': 'reports', 'username': None, 'password': None, 'client_id': None, 'client_secret': None, 'use_app_authentication': False, 'file_pattern': '*.xlsx', 'overwrite': True }, 'scheduler': { 'enabled': False, 'schedule_type': 'cron', # 'interval', 'cron', or 'once' 'interval_hours': 24, # For interval type 'cron_expression': '0 10 * * *', # For cron type (10 AM EST/EDT daily) 'timezone': 'America/New_York' # EST/EDT timezone }, 'api': { 'enabled': False, 'host': '0.0.0.0', 'port': 8080, 'api_key': None # Optional API key for authentication }, 'report': { 'output_dir': 'output', 'reports_dir': 'reports' } } def load_config(config_path: Optional[str] = None) -> Dict[str, Any]: """ Load configuration from YAML file or environment variables. Args: config_path: Path to config.yaml file (default: config.yaml in current directory) Returns: Configuration dictionary """ # Load .env file if available (from current directory or parent taskboard directory) if DOTENV_AVAILABLE: # Try loading from vendor_report/.env first env_file = Path(__file__).parent / ".env" if not env_file.exists(): # Try loading from parent taskboard/.env parent_env = Path(__file__).parent.parent / "taskboard" / ".env" if parent_env.exists(): env_file = parent_env logging.info(f"Found .env file in taskboard directory: {env_file}") else: logging.warning(f".env file not found in vendor_report or taskboard directory") logging.warning(f"Checked: {Path(__file__).parent / '.env'}") logging.warning(f"Checked: {parent_env}") else: logging.info(f"Found .env file in vendor_report directory: {env_file}") if env_file.exists(): load_dotenv(env_file, override=True) # override=True ensures env vars take precedence logging.info(f"Loaded environment variables from {env_file.absolute()}") # Log which SharePoint env vars were found (checking both SHAREPOINT_* and AZURE_AD_* fallbacks) sp_vars = ['SHAREPOINT_ENABLED', 'SHAREPOINT_SITE_URL', 'SHAREPOINT_FOLDER_PATH'] found_vars = [var for var in sp_vars if os.getenv(var)] # Check credentials (with fallback) client_id = os.getenv('SHAREPOINT_CLIENT_ID') or os.getenv('AZURE_AD_CLIENT_ID') tenant_id = os.getenv('SHAREPOINT_TENANT_ID') or os.getenv('AZURE_AD_TENANT_ID') client_secret = os.getenv('SHAREPOINT_CLIENT_SECRET') or os.getenv('AZURE_AD_CLIENT_SECRET') if client_id: found_vars.append('CLIENT_ID (from SHAREPOINT_CLIENT_ID or AZURE_AD_CLIENT_ID)') if tenant_id: found_vars.append('TENANT_ID (from SHAREPOINT_TENANT_ID or AZURE_AD_TENANT_ID)') if client_secret: found_vars.append('CLIENT_SECRET (from SHAREPOINT_CLIENT_SECRET or AZURE_AD_CLIENT_SECRET)') logging.info(f"Found SharePoint environment variables: {', '.join(found_vars)}") missing_vars = [] if not client_id: missing_vars.append('CLIENT_ID (SHAREPOINT_CLIENT_ID or AZURE_AD_CLIENT_ID)') if not tenant_id: missing_vars.append('TENANT_ID (SHAREPOINT_TENANT_ID or AZURE_AD_TENANT_ID)') if not client_secret: missing_vars.append('CLIENT_SECRET (SHAREPOINT_CLIENT_SECRET or AZURE_AD_CLIENT_SECRET)') if missing_vars: logging.warning(f"Missing SharePoint credentials: {', '.join(missing_vars)}") if config_path is None: config_path = Path(__file__).parent / "config.yaml" else: config_path = Path(config_path) config = DEFAULT_CONFIG.copy() # Load from YAML file if exists if config_path.exists(): try: with open(config_path, 'r') as f: file_config = yaml.safe_load(f) or {} # Deep merge with defaults config = _deep_merge(config, file_config) except Exception as e: print(f"Warning: Failed to load config from {config_path}: {e}") # Override with environment variables config = _load_from_env(config) return config def _deep_merge(base: Dict, override: Dict) -> Dict: """Deep merge two dictionaries.""" result = base.copy() for key, value in override.items(): if key in result and isinstance(result[key], dict) and isinstance(value, dict): result[key] = _deep_merge(result[key], value) else: result[key] = value return result def _load_from_env(config: Dict) -> Dict: """Load configuration from environment variables.""" # SharePoint settings if os.getenv('SHAREPOINT_ENABLED'): config['sharepoint']['enabled'] = os.getenv('SHAREPOINT_ENABLED').lower() == 'true' if os.getenv('SHAREPOINT_SITE_URL'): config['sharepoint']['site_url'] = os.getenv('SHAREPOINT_SITE_URL') if os.getenv('SHAREPOINT_FOLDER_PATH'): config['sharepoint']['folder_path'] = os.getenv('SHAREPOINT_FOLDER_PATH') if os.getenv('SHAREPOINT_USERNAME'): config['sharepoint']['username'] = os.getenv('SHAREPOINT_USERNAME') if os.getenv('SHAREPOINT_PASSWORD'): config['sharepoint']['password'] = os.getenv('SHAREPOINT_PASSWORD') # Check for SHAREPOINT_CLIENT_ID first, fallback to AZURE_AD_CLIENT_ID if os.getenv('SHAREPOINT_CLIENT_ID'): config['sharepoint']['client_id'] = os.getenv('SHAREPOINT_CLIENT_ID') elif os.getenv('AZURE_AD_CLIENT_ID'): config['sharepoint']['client_id'] = os.getenv('AZURE_AD_CLIENT_ID') # Check for SHAREPOINT_CLIENT_SECRET first, fallback to AZURE_AD_CLIENT_SECRET if os.getenv('SHAREPOINT_CLIENT_SECRET'): config['sharepoint']['client_secret'] = os.getenv('SHAREPOINT_CLIENT_SECRET') elif os.getenv('AZURE_AD_CLIENT_SECRET'): config['sharepoint']['client_secret'] = os.getenv('AZURE_AD_CLIENT_SECRET') # Tenant ID (required for Microsoft Graph API) if os.getenv('SHAREPOINT_TENANT_ID'): config['sharepoint']['tenant_id'] = os.getenv('SHAREPOINT_TENANT_ID') elif os.getenv('AZURE_AD_TENANT_ID'): config['sharepoint']['tenant_id'] = os.getenv('AZURE_AD_TENANT_ID') if os.getenv('SHAREPOINT_USE_APP_AUTH'): config['sharepoint']['use_app_authentication'] = os.getenv('SHAREPOINT_USE_APP_AUTH').lower() == 'true' elif os.getenv('SHAREPOINT_USE_APP_AUTH') is None and os.getenv('AZURE_AD_CLIENT_ID'): # If Azure AD credentials are present, default to app auth config['sharepoint']['use_app_authentication'] = True # Scheduler settings if os.getenv('SCHEDULER_ENABLED'): config['scheduler']['enabled'] = os.getenv('SCHEDULER_ENABLED').lower() == 'true' if os.getenv('SCHEDULER_SCHEDULE_TYPE'): config['scheduler']['schedule_type'] = os.getenv('SCHEDULER_SCHEDULE_TYPE') if os.getenv('SCHEDULER_INTERVAL_HOURS'): config['scheduler']['interval_hours'] = int(os.getenv('SCHEDULER_INTERVAL_HOURS')) if os.getenv('SCHEDULER_CRON'): config['scheduler']['cron_expression'] = os.getenv('SCHEDULER_CRON') if os.getenv('SCHEDULER_TIMEZONE'): config['scheduler']['timezone'] = os.getenv('SCHEDULER_TIMEZONE') # API settings if os.getenv('API_ENABLED'): config['api']['enabled'] = os.getenv('API_ENABLED').lower() == 'true' if os.getenv('API_PORT'): config['api']['port'] = int(os.getenv('API_PORT')) if os.getenv('API_HOST'): config['api']['host'] = os.getenv('API_HOST') if os.getenv('API_KEY'): config['api']['api_key'] = os.getenv('API_KEY') # Report settings if os.getenv('REPORT_OUTPUT_DIR'): config['report']['output_dir'] = os.getenv('REPORT_OUTPUT_DIR') if os.getenv('REPORT_REPORTS_DIR'): config['report']['reports_dir'] = os.getenv('REPORT_REPORTS_DIR') return config def save_config_template(config_path: Optional[str] = None) -> None: """Save a template configuration file.""" if config_path is None: config_path = Path(__file__).parent / "config.yaml.template" else: config_path = Path(config_path) template = """# Vendor Report Generator Configuration # SharePoint Integration sharepoint: enabled: false # Set to true to enable SharePoint downloads site_url: "https://yourcompany.sharepoint.com/sites/YourSite" folder_path: "/Shared Documents/Reports" # Path to folder containing Excel files # file_path: "/Shared Documents/Reports/file.xlsx" # Alternative: single file path local_dir: "reports" # Local directory to save downloaded files username: null # Username for user authentication (leave null if using app auth) password: null # Password for user authentication (leave null if using app auth) client_id: null # Azure AD app client ID (for app authentication) client_secret: null # Azure AD app client secret (for app authentication) use_app_authentication: false # Set to true to use app authentication (recommended) file_pattern: "*.xlsx" # Pattern to filter files overwrite: true # Whether to overwrite existing files # Scheduler Configuration scheduler: enabled: false # Set to true to enable scheduled report generation schedule_type: "interval" # Options: "interval", "cron", or "once" interval_hours: 24 # For interval type: generate report every N hours cron_expression: "0 8 * * *" # For cron type: generate at 8 AM daily (cron format) timezone: "America/New_York" # Timezone for scheduling # API Configuration (for on-demand report generation) api: enabled: false # Set to true to enable web API host: "0.0.0.0" # Host to bind API server port: 8080 # Port for API server api_key: null # Optional API key for authentication (set to enable auth) # Report Settings report: output_dir: "output" # Directory for generated reports reports_dir: "reports" # Directory containing Excel files """ with open(config_path, 'w') as f: f.write(template) print(f"Configuration template saved to: {config_path}")