124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
#!/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'] |