"""Base DB adapter.""" from functools import lru_cache from importlib import import_module from types import ModuleType from typing import Dict, cast from sqlalchemy.engine import Connection, Engine from sqlalchemy.orm import Session, scoped_session from .adapter import Adapter from .dialect import Dialect from .utils import HasDialect # Map dialect name -> module import path _ADAPTERS: Dict[Dialect, str] = { 'mysql': '.mysql_adapter:MySQLAdapter', 'sqlite': '.sqlite_adapter:SQLiteAdapter', } # --- Public --- def get_adapter( target: Connection | Engine | Dialect | HasDialect | Session | scoped_session, ) -> Adapter: """Get the DB adapter based on dialect.""" return _get_adapter(get_dialect(target)) def get_dialect( target: Connection | Engine | Dialect | HasDialect | Session | scoped_session, ) -> Dialect: """Get the DB dialect (e.g., mysql, sqlite).""" # string if isinstance(target, str): return _try_dialect(target) # Scoped session -> Session if isinstance(target, scoped_session): target = target() # type: ignore # Session -> Connection if isinstance(target, Session): target = target.get_bind() # type: ignore if target is None: raise RuntimeError('Session is not bound to an engine/connection.') # Engine or Connection if isinstance(target, (Engine, Connection)): return _try_dialect(target.dialect.name) # type: ignore # Duck-typing: HasDialect d = getattr(target, 'dialect', None) if d is not None and getattr(d, 'name', None): dialect = str(d.name) else: raise TypeError(f'Unknown target type {type(target)!r}') return dialect # type: ignore def is_dialect(dialect: str) -> bool: """Check if dialect is supported.""" return dialect in _ADAPTERS # --- Testing Overrides --- _overrides: Dict[Dialect, Adapter] = {} def clear_adapter_cache() -> None: """Clear the lazy cache.""" _load_adapter.cache_clear() def clear_adapter_override(dialect: Dialect | None = None) -> None: """Clear the overrides.""" if dialect is None: _overrides.clear() else: _overrides.pop(dialect, None) clear_adapter_cache() def set_adapter_override(dialect: Dialect, adapter: Adapter) -> None: """Inject a custom adapter (tests); clears cache so it takes effect.""" _overrides[dialect] = adapter clear_adapter_cache() # --- Helpers --- def _get_adapter(dialect: Dialect) -> Adapter: if dialect in _overrides: return _overrides[dialect] return _load_adapter(dialect) def _import_module(mod_path: str) -> ModuleType: # Resolve relative imports if mod_path.startswith('.'): return import_module(mod_path, package=__package__) return import_module(mod_path) @lru_cache(maxsize=None) def _load_adapter(dialect: Dialect) -> Adapter: """Lazily import and instantiate a dialect adapter.""" # Check for known dialect if dialect not in _ADAPTERS: _raise_unknown_dialect(dialect) # Import the module mod_path, _, cls_name = _ADAPTERS[dialect].partition(':') try: mod = _import_module(mod_path) except Exception as e: raise RuntimeError( f'Failed to import adapter module {mod_path!r} for {dialect!r}' ) from e # Get the class try: cls = getattr(mod, cls_name) except AttributeError as e: raise RuntimeError( f'Adapter class {cls_name!r} not found in {mod_path!r} for {dialect!r}' ) from e # Create the instance adapter = cast(Adapter, cls()) # Sanity check: instance dialect matches mod_dialect = adapter.dialect.name if mod_dialect != dialect: raise RuntimeError( f'Adapter {adapter!r} declares dialect.name={mod_dialect!r},' f' expected {dialect!r}' ) return adapter def _raise_unknown_dialect(dialect: object) -> None: supported = ', '.join(sorted(_ADAPTERS)) raise TypeError(f'Unknown dialect {dialect!r}. Supported: {supported}') def _try_dialect(dialect: str) -> Dialect: if not is_dialect(dialect): _raise_unknown_dialect(dialect) return cast(Dialect, dialect)