import json import os from datetime import datetime from google_drive_manager import GoogleDriveManager import streamlit as st class ConfigManager: def __init__(self): self.config_file = "wedding_config.json" self.data_dir = "data" self.app_config_file = "app_config.json" self.demo_data_dir = "demo_data" # Check if running on Hugging Face Spaces self.is_huggingface = os.getenv('SPACE_ID') is not None # Initialize Google Drive manager self.drive_manager = GoogleDriveManager() self.google_drive_enabled = False # For Hugging Face Spaces, use in-memory storage if self.is_huggingface: self.use_memory_storage = True self.memory_data = {} self.data_loaded_from_drive = False # Track if data has been loaded else: self.use_memory_storage = False self.data_loaded_from_drive = False # Set up data directory for local development try: if not os.path.exists(self.data_dir): os.makedirs(self.data_dir, exist_ok=True) if not os.path.exists(self.demo_data_dir): os.makedirs(self.demo_data_dir, exist_ok=True) except Exception as e: print(f"Error creating directories: {e}") # Load app configuration self.app_config = self.load_app_config() # Initialize Google Drive if enabled self._initialize_google_drive() def get_user_folder(self): """Get the user-specific folder name""" # Use demo_data folder if in demo mode, otherwise use laraandumang if self.is_demo_mode(): return "demo_data" return "laraandumang" def set_user_folder(self, folder_name): """Set the user-specific folder name""" self.user_folder = folder_name def get_current_user_folder(self): """Get the currently set user folder""" return getattr(self, 'user_folder', self.get_user_folder()) def _initialize_google_drive(self): """Initialize Google Drive connection""" try: folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID') if folder_id: if self.drive_manager.initialize(folder_id): self.google_drive_enabled = True # Don't automatically sync on startup - let user choose print("Google Drive initialized successfully. Manual sync required.") except Exception as e: print(f"Google Drive initialization failed: {e}") self.google_drive_enabled = False def _sync_from_google_drive(self): """Sync data files from Google Drive to local storage""" if not self.google_drive_enabled: return try: # Get user-specific folder user_folder = self.get_current_user_folder() # List of data files to sync data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json' ] # Sync config file from user folder config_content = self.drive_manager.download_file(f'{user_folder}/wedding_config.json') if config_content: if self.use_memory_storage: # Store in memory for Hugging Face Spaces self.memory_data['wedding_config.json'] = config_content # Update cache in session state st.session_state['cached_wedding_config'] = config_content print("✅ Loaded wedding configuration from Google Drive") else: # Store in file for local development config_path = self.get_config_file_path() config_dir = os.path.dirname(config_path) if config_dir: os.makedirs(config_dir, exist_ok=True) with open(config_path, 'w') as f: json.dump(config_content, f, indent=2) # Update cache in session state st.session_state['cached_wedding_config'] = config_content print(f"Stored wedding_config.json in file: {config_path}") # Sync data files from user folder for file_name in data_files: content = self.drive_manager.download_file(f'{user_folder}/{file_name}') if content: if self.use_memory_storage: # Store in memory for Hugging Face Spaces self.memory_data[file_name] = content # Update cache in session state cache_key = f"cached_{file_name}" st.session_state[cache_key] = content # Only print once per sync operation if file_name == data_files[0]: # First file print(f"✅ Loaded {len(data_files)} data files from Google Drive") else: # Store in file for local development file_path = self.get_data_file_path(file_name) file_dir = os.path.dirname(file_path) if file_dir: os.makedirs(file_dir, exist_ok=True) with open(file_path, 'w') as f: json.dump(content, f, indent=2) # Update cache in session state cache_key = f"cached_{file_name}" st.session_state[cache_key] = content print(f"Stored {file_name} in file: {file_path}") except Exception as e: print(f"Error syncing from Google Drive: {e}") # Don't fail the entire initialization if sync fails # This is expected behavior - user can manually sync later def _sync_to_google_drive(self): """Sync local data files to Google Drive""" if not self.google_drive_enabled: return # Get user-specific folder user_folder = self.get_user_folder() # Sync config file to user folder config_path = self.get_config_file_path() if os.path.exists(config_path): with open(config_path, 'r') as f: config_content = json.load(f) self.drive_manager.upload_file(f'{user_folder}/wedding_config.json', config_content) # Sync data files to user folder data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json' ] for file_name in data_files: file_path = self.get_data_file_path(file_name) if os.path.exists(file_path): with open(file_path, 'r') as f: content = json.load(f) self.drive_manager.upload_file(f'{user_folder}/{file_name}', content) def load_app_config(self): """Load app configuration from file""" # For Hugging Face Spaces, check /tmp directory first if self.is_huggingface: tmp_config_path = f"/tmp/{self.app_config_file}" if os.path.exists(tmp_config_path): try: with open(tmp_config_path, 'r') as f: return json.load(f) except (json.JSONDecodeError, FileNotFoundError): pass # Check original location if os.path.exists(self.app_config_file): try: with open(self.app_config_file, 'r') as f: return json.load(f) except (json.JSONDecodeError, FileNotFoundError): return self.get_default_app_config() return self.get_default_app_config() def get_default_app_config(self): """Get default app configuration""" return { "demo_mode": False, "demo_data_path": "demo_data", "app_settings": { "theme": "green_adirondack", "auto_save": True, "backup_enabled": True } } def is_demo_mode(self): """Check if demo mode is enabled""" return self.app_config.get("demo_mode", False) def get_data_directory(self): """Get the appropriate data directory based on demo mode""" if self.is_demo_mode(): return self.demo_data_dir return self.data_dir def get_config_file_path(self): """Get the appropriate config file path based on demo mode""" if self.is_demo_mode(): return os.path.join(self.demo_data_dir, "wedding_config.json") # For Hugging Face Spaces, use app directory to avoid permission issues if self.is_huggingface: return "wedding_config.json" # For local development, use the original path if os.path.isabs(self.config_file): return self.config_file else: return os.path.join(os.getcwd(), self.config_file) def config_exists(self): """Check if configuration file exists""" if self.use_memory_storage: return 'wedding_config.json' in self.memory_data else: config_path = self.get_config_file_path() return os.path.exists(config_path) def load_config(self): """Load configuration from cache, file or memory""" # Check if we have cached config in session state if 'cached_wedding_config' in st.session_state: return st.session_state['cached_wedding_config'] if self.config_exists(): try: if self.use_memory_storage: config = self.memory_data['wedding_config.json'] else: config_path = self.get_config_file_path() with open(config_path, 'r') as f: config = json.load(f) # Cache the config in session state st.session_state['cached_wedding_config'] = config return config except (json.JSONDecodeError, FileNotFoundError, KeyError): default_config = self.get_default_config() st.session_state['cached_wedding_config'] = default_config return default_config default_config = self.get_default_config() st.session_state['cached_wedding_config'] = default_config return default_config def save_config(self, config): """Save configuration to file or memory""" try: if self.use_memory_storage: # Store in memory for Hugging Face Spaces self.memory_data['wedding_config.json'] = config else: # Store in file for local development config_path = self.get_config_file_path() with open(config_path, 'w') as f: json.dump(config, f, indent=2) # Update cache in session state st.session_state['cached_wedding_config'] = config # Mark config as modified for manual sync st.session_state['config_modified'] = True return True except Exception as e: print(f"Error saving config: {e}") return False def reset_config(self): """Reset configuration by deleting the config file""" try: config_path = self.get_config_file_path() if os.path.exists(config_path): os.remove(config_path) return True except Exception as e: print(f"Error resetting config: {e}") return False def get_default_config(self): """Get default configuration""" return { 'wedding_info': { 'partner1_name': '', 'partner2_name': '', 'wedding_start_date': '', 'wedding_end_date': '', 'venue_city': '' }, 'custom_settings': { 'custom_tags': [ "Wedding Party", "Urgent", "Rehearsal", "Timeline", "Maid of Honor", "Best Man", "Bridesmaid", "Groomsman", "Flower Girl", "Ring Bearer", "Usher", "Reader", "Venue", "Catering", "Photography", "Videography", "Music/DJ", "Flowers", "Decor", "Attire", "Hair & Makeup", "Transportation", "Invitations", "Cake", "Officiant", "Other", "Decorations", "Centerpieces", "Favors", "Signage", "Linens", "Tableware", "Lighting", "Accessories", "Stationery", "Gifts", "Vendor", "Item", "Deposit Required", "Final Payment", "Installment", "Food & Beverage", "Music", "Entertainment", "Lodging" ], 'task_assignees': [] }, 'wedding_events': [ {"name": "Welcome Dinner", "date_offset": -1, "description": "Welcome dinner for out-of-town guests", "requires_meal_choice": True, "meal_options": ["Duck", "Surf & Turf", "Risotto (vegetarian)", "Stuffed Squash (vegetarian)"], "location": ""}, {"name": "Church Ceremony", "date_offset": 0, "description": "Main wedding ceremony", "requires_meal_choice": False, "meal_options": [], "location": ""}, {"name": "Reception", "date_offset": 0, "description": "Wedding reception with dinner", "requires_meal_choice": True, "meal_options": ["Duck", "Surf & Turf", "Risotto (vegetarian)", "Stuffed Squash (vegetarian)"], "location": ""}, {"name": "Mehndi Afterparty", "date_offset": 1, "description": "Mehndi celebration and afterparty", "requires_meal_choice": False, "meal_options": [], "location": ""}, {"name": "Indian Ceremony", "date_offset": 1, "description": "Traditional Indian wedding ceremony", "requires_meal_choice": False, "meal_options": [], "location": ""}, {"name": "Indian Reception", "date_offset": 1, "description": "Indian reception celebration", "requires_meal_choice": True, "meal_options": ["Duck", "Surf & Turf", "Risotto (vegetarian)", "Stuffed Squash (vegetarian)"], "location": ""} ] } def get_data_file_path(self, filename): """Get full path for data files""" data_dir = self.get_data_directory() return os.path.join(data_dir, filename) def load_json_data(self, filename): """Load JSON data from cache, data directory, or memory""" # Check if we have cached data in session state cache_key = f"cached_{filename}" if cache_key in st.session_state: # Data loaded from cache return st.session_state[cache_key] # Load data from source if self.use_memory_storage: # Load from memory for Hugging Face Spaces data = self.memory_data.get(filename, []) else: # Load from file for local development filepath = self.get_data_file_path(filename) if os.path.exists(filepath): try: with open(filepath, 'r') as f: data = json.load(f) except (json.JSONDecodeError, FileNotFoundError): data = [] else: data = [] # Cache the data in session state st.session_state[cache_key] = data return data def save_json_data(self, filename, data): """Save JSON data to data directory or memory""" try: if self.use_memory_storage: # Store in memory for Hugging Face Spaces self.memory_data[filename] = data else: # Store in file for local development filepath = self.get_data_file_path(filename) with open(filepath, 'w') as f: json.dump(data, f, indent=2) # Update cache in session state cache_key = f"cached_{filename}" st.session_state[cache_key] = data # Mark data as modified for manual sync st.session_state[f'{filename}_modified'] = True return True except Exception as e: print(f"Error saving data to {filename}: {e}") return False def toggle_demo_mode(self): """Toggle demo mode on/off""" self.app_config["demo_mode"] = not self.app_config.get("demo_mode", False) try: # For Hugging Face Spaces, use /tmp directory to avoid permission issues if self.is_huggingface: config_path = f"/tmp/{self.app_config_file}" else: config_path = self.app_config_file with open(config_path, 'w') as f: json.dump(self.app_config, f, indent=2) return True except Exception as e: print(f"Error saving app config: {e}") return False def set_demo_mode(self, enabled): """Set demo mode to specific value""" self.app_config["demo_mode"] = enabled try: # For Hugging Face Spaces, use /tmp directory to avoid permission issues if self.is_huggingface: config_path = f"/tmp/{self.app_config_file}" else: config_path = self.app_config_file with open(config_path, 'w') as f: json.dump(self.app_config, f, indent=2) return True except Exception as e: print(f"Error saving app config: {e}") return False def is_google_drive_enabled(self): """Check if Google Drive integration is enabled and working""" return self.google_drive_enabled and self.drive_manager.is_online() def manual_sync_to_drive(self): """Manually sync all data to Google Drive""" if not self.google_drive_enabled: return False try: self._sync_to_google_drive() return True except Exception as e: print(f"Error syncing to Google Drive: {e}") return False def clear_cache(self): """Clear all cached data from session state""" data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json', 'guests.json' ] for filename in data_files: cache_key = f"cached_{filename}" if cache_key in st.session_state: del st.session_state[cache_key] # Also clear config cache if 'cached_wedding_config' in st.session_state: del st.session_state['cached_wedding_config'] def get_cache_status(self): """Get information about what data is currently cached""" data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json', 'guests.json' ] cached_files = [] for filename in data_files: cache_key = f"cached_{filename}" if cache_key in st.session_state: cached_files.append(filename) config_cached = 'cached_wedding_config' in st.session_state return { 'cached_data_files': cached_files, 'config_cached': config_cached, 'total_cached': len(cached_files) + (1 if config_cached else 0) } def invalidate_cache_for_file(self, filename): """Invalidate cache for a specific file""" cache_key = f"cached_{filename}" if cache_key in st.session_state: del st.session_state[cache_key] def invalidate_config_cache(self): """Invalidate the config cache""" if 'cached_wedding_config' in st.session_state: del st.session_state['cached_wedding_config'] def manual_sync_from_drive(self): """Manually sync all data from Google Drive""" if not self.google_drive_enabled: return False try: self._sync_from_google_drive() self.data_loaded_from_drive = True # Clear modified flags since data is now in sync with Google Drive self.clear_modified_flags() return True except Exception as e: print(f"Error syncing from Google Drive: {e}") return False def load_existing_data_from_drive(self): """Load existing wedding data from Google Drive and create local config""" if not self.google_drive_enabled: return False try: # Disable demo mode when loading real data self.set_demo_mode(False) # Get user-specific folder user_folder = self.get_current_user_folder() # Check if wedding_config.json exists in Google Drive user folder config_content = self.drive_manager.download_file(f'{user_folder}/wedding_config.json') if config_content: # Save config locally (in memory or file) if self.use_memory_storage: self.memory_data['wedding_config.json'] = config_content # Update cache in session state st.session_state['cached_wedding_config'] = config_content print("Stored wedding_config.json in memory") else: config_path = self.get_config_file_path() config_dir = os.path.dirname(config_path) if config_dir: os.makedirs(config_dir, exist_ok=True) with open(config_path, 'w') as f: json.dump(config_content, f, indent=2) # Update cache in session state st.session_state['cached_wedding_config'] = config_content print(f"Stored wedding_config.json in file: {config_path}") # Load data files from user folder data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json' ] for file_name in data_files: content = self.drive_manager.download_file(f'{user_folder}/{file_name}') if content: if self.use_memory_storage: self.memory_data[file_name] = content # Update cache in session state cache_key = f"cached_{file_name}" st.session_state[cache_key] = content print(f"Stored {file_name} in memory") else: file_path = self.get_data_file_path(file_name) file_dir = os.path.dirname(file_path) if file_dir: os.makedirs(file_dir, exist_ok=True) with open(file_path, 'w') as f: json.dump(content, f, indent=2) # Update cache in session state cache_key = f"cached_{file_name}" st.session_state[cache_key] = content print(f"Stored {file_name} in file: {file_path}") self.data_loaded_from_drive = True return True return False except Exception as e: print(f"Error loading existing data from Google Drive: {e}") # This is a common issue on Hugging Face Spaces due to network/SSL issues # The error is expected and the user can retry manually return False def load_demo_data_from_drive(self): """Load demo wedding data from Google Drive and enable demo mode""" if not self.google_drive_enabled: return False try: # Enable demo mode first self.set_demo_mode(True) # Load demo data from demo folder in Google Drive demo_folder = self.get_current_user_folder() # Will return "demo_data" since demo mode is enabled # Check if wedding_config.json exists in demo folder config_content = self.drive_manager.download_file(f'{demo_folder}/wedding_config.json') if config_content: # Save config locally (in memory or file) if self.use_memory_storage: self.memory_data['wedding_config.json'] = config_content # Update cache in session state st.session_state['cached_wedding_config'] = config_content print("Stored demo wedding_config.json in memory") else: config_path = self.get_config_file_path() config_dir = os.path.dirname(config_path) if config_dir: os.makedirs(config_dir, exist_ok=True) with open(config_path, 'w') as f: json.dump(config_content, f, indent=2) # Update cache in session state st.session_state['cached_wedding_config'] = config_content print(f"Stored demo wedding_config.json in file: {config_path}") # Load demo data files from demo folder data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json' ] for file_name in data_files: content = self.drive_manager.download_file(f'{demo_folder}/{file_name}') if content: if self.use_memory_storage: self.memory_data[file_name] = content # Update cache in session state cache_key = f"cached_{file_name}" st.session_state[cache_key] = content print(f"Stored demo {file_name} in memory") else: file_path = self.get_data_file_path(file_name) file_dir = os.path.dirname(file_path) if file_dir: os.makedirs(file_dir, exist_ok=True) with open(file_path, 'w') as f: json.dump(content, f, indent=2) # Update cache in session state cache_key = f"cached_{file_name}" st.session_state[cache_key] = content print(f"Stored demo {file_name} in file: {file_path}") self.data_loaded_from_drive = True return True return False except Exception as e: print(f"Error loading demo data from Google Drive: {e}") # This is a common issue on Hugging Face Spaces due to network/SSL issues # The error is expected and the user can retry manually return False def get_google_drive_status(self): """Get status information about Google Drive integration""" if not self.google_drive_enabled: return { 'enabled': False, 'status': 'Disabled', 'message': 'Google Drive integration not configured' } if not self.drive_manager.is_online(): return { 'enabled': True, 'status': 'Offline', 'message': 'Google Drive service unavailable' } try: files = self.drive_manager.list_files() return { 'enabled': True, 'status': 'Online', 'message': f'Connected to Google Drive ({len(files)} files found)', 'files': [f['name'] for f in files] } except Exception as e: return { 'enabled': True, 'status': 'Error', 'message': f'Error connecting to Google Drive: {str(e)}' } def get_modified_files(self): """Get list of files that have been modified since last sync""" modified_files = [] # Check config if st.session_state.get('config_modified', False): modified_files.append('wedding_config.json') # Check data files data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json' ] for filename in data_files: if st.session_state.get(f'{filename}_modified', False): modified_files.append(filename) return modified_files def clear_modified_flags(self): """Clear all modified flags after successful sync""" st.session_state['config_modified'] = False data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json' ] for filename in data_files: st.session_state[f'{filename}_modified'] = False def manual_sync_to_drive(self): """Manually sync all current data to Google Drive""" if not self.google_drive_enabled: return False try: # Get user-specific folder user_folder = self.get_current_user_folder() modified_files = self.get_modified_files() if not modified_files: print("No modified files detected, but syncing all current data to ensure consistency") else: print(f"Syncing {len(modified_files)} modified files to Google Drive: {modified_files}") # Always sync config (current state) to user folder config = self.load_config() if not self.drive_manager.upload_file(f'{user_folder}/wedding_config.json', config): return False # Always sync all data files (current state) to user folder data_files = [ 'guest_list_data.json', 'rsvp_data.json', 'tasks.json', 'vendors.json', 'wedding_party.json' ] for filename in data_files: data = self.load_json_data(filename) if not self.drive_manager.upload_file(f'{user_folder}/{filename}', data): return False # Clear modified flags after successful sync self.clear_modified_flags() return True except Exception as e: print(f"Error syncing to Google Drive: {e}") return False def reset_app_state(self): """Reset the app state by clearing all cached data and memory storage""" try: # Clear memory data if hasattr(self, 'memory_data'): self.memory_data.clear() # Clear session state cache if 'cached_wedding_config' in st.session_state: del st.session_state['cached_wedding_config'] # Clear other cached data files for key in list(st.session_state.keys()): if key.startswith('cached_'): del st.session_state[key] # Reset data loaded flag self.data_loaded_from_drive = False print("App state reset successfully") return True except Exception as e: print(f"Error resetting app state: {e}") return False