#!/usr/bin/env python3 """ Central Model Registry for Service Finder Automatically discovers and imports all SQLAlchemy models from the models directory, ensuring Base.metadata is fully populated with tables, constraints, and indexes. Usage: from app.models.registry import Base, get_all_models, ensure_models_loaded """ import importlib import os import sys from pathlib import Path from typing import Dict, List, Type from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm import DeclarativeBase # Import the Base from database (circular dependency will be resolved later) # We'll define our own Base if needed, but better to reuse existing one. # We'll import after path setup. # Add backend to path if not already backend_dir = Path(__file__).parent.parent.parent if str(backend_dir) not in sys.path: sys.path.insert(0, str(backend_dir)) # Import Base from database (this will be the same Base used everywhere) from app.database import Base def discover_model_files() -> List[Path]: """ Walk through models directory and collect all .py files except __init__.py and registry.py. """ models_dir = Path(__file__).parent model_files = [] for root, _, files in os.walk(models_dir): for file in files: if file.endswith('.py') and file not in ('__init__.py', 'registry.py'): full_path = Path(root) / file model_files.append(full_path) return model_files def import_module_from_file(file_path: Path) -> str: """ Import a Python module from its file path. Returns the module name. """ # Compute module name relative to backend/app rel_path = file_path.relative_to(backend_dir) module_name = str(rel_path).replace(os.sep, '.').replace('.py', '') try: spec = importlib.util.spec_from_file_location(module_name, file_path) if spec is None: raise ImportError(f"Could not load spec for {module_name}") module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) return module_name except Exception as e: # Silently skip import errors (maybe due to missing dependencies) # but log for debugging print(f"⚠️ Could not import {module_name}: {e}", file=sys.stderr) return None def load_all_models() -> List[str]: """ Dynamically import all model files to populate Base.metadata. Returns list of successfully imported module names. """ model_files = discover_model_files() imported = [] for file in model_files: module_name = import_module_from_file(file) if module_name: imported.append(module_name) # Also ensure the __init__.py is loaded (it imports many models manually) try: import app.models imported.append('app.models') except ImportError: pass print(f"✅ Registry loaded {len(imported)} model modules. Total tables in metadata: {len(Base.metadata.tables)}") return imported def get_all_models() -> Dict[str, Type[DeclarativeMeta]]: """ Return a mapping of class name to model class for all registered SQLAlchemy models. This works only after models have been imported. """ # This is a heuristic: find all subclasses of Base in loaded modules from sqlalchemy.orm import DeclarativeBase models = {} for cls in Base.__subclasses__(): models[cls.__name__] = cls # Also check deeper inheritance (if models inherit from other models that inherit from Base) for module_name, module in sys.modules.items(): if module_name.startswith('app.models.'): for attr_name in dir(module): attr = getattr(module, attr_name) if isinstance(attr, type) and issubclass(attr, Base) and attr is not Base: models[attr.__name__] = attr return models def ensure_models_loaded(): """ Ensure that all models are loaded into Base.metadata. This is idempotent and can be called multiple times. """ if len(Base.metadata.tables) == 0: load_all_models() else: # Already loaded pass # Auto-load models when this module is imported (optional, but useful) # We'll make it explicit via a function call to avoid side effects. # Instead, we'll provide a function to trigger loading. # Export __all__ = ['Base', 'discover_model_files', 'load_all_models', 'get_all_models', 'ensure_models_loaded']