Source code for edri.integrations.sqlalchemy.event

from __future__ import annotations

from dataclasses import dataclass
from types import UnionType
from typing import Any, ClassVar, TypeVar, Type, get_type_hints, get_origin, get_args, Union

from edri.dataclass.event import Event
from edri.integrations.sqlalchemy._core import ForeignKeyValidator
from edri.integrations.sqlalchemy.session import register_orm_session
from edri.utility.function import Injection
from sqlalchemy.orm import Session


__all__ = ["ORMEvent", "orm_event"]

T = TypeVar("T", bound="ORMEvent")


[docs] @dataclass class ORMEvent(Event): # per-class: cache with information about which fields are ORM / list-of-ORM etc. # example: {"information": "single", "parents": "list", "role_group": "list", ...} _orm_fields: ClassVar[dict[str, str]] = {} # --------- class-level helpers (run once per subclass) --------- @classmethod def _init_orm_field_cache(cls) -> None: """ Initialize _orm_fields for this subclass. Intended to be called from the @orm_event decorator. """ if cls._orm_fields: return # already initialized # include_extras=True so generics / future stuff survives hints = get_type_hints(cls, include_extras=True) orm_field_kinds: dict[str, str] = {} for name, annotation in hints.items(): kind = cls._classify_orm_field(annotation) if kind is not None: orm_field_kinds[name] = kind cls._orm_fields = orm_field_kinds @classmethod def _classify_orm_field(cls, annotation: Any) -> str | None: """ Decide if given annotation is - a single FK -> returns "single" - a list[FK] -> returns "list" - else -> returns None """ origin = get_origin(annotation) # --- NEW: unwrap Optional / Union[T, None] --- if origin is Union or origin is UnionType: args = [a for a in get_args(annotation) if a is not type(None)] if len(args) == 1: # Re-run classification on the inner type return cls._classify_orm_field(args[0]) return None # multi-type unions not supported # Handle list[inject(ForeignKeyValidator[Model], session_factory=SessionFactory)] if origin is list: (inner,) = get_args(annotation) if cls._is_injection_fk(inner): return "list" return None # Handle plain inject(ForeignKeyValidator[Model], session_factory=SessionFactory) if cls._is_injection_fk(annotation): return "single" return None @staticmethod def _is_injection_fk(annotation: Any) -> bool: """ True if the annotation is an Injection that contains a ForeignKeyValidator[...] class in its 'classes'. """ if not isinstance(annotation, Injection): return False for injected_cls in annotation.classes: if ORMEvent._is_fk_type(injected_cls): return True return False @staticmethod def _is_fk_type(tp: Any) -> bool: """ True if tp is ForeignKeyValidator[...] or a subclass thereof. """ # Handles things like ForeignKeyValidator[Model] origin = get_origin(tp) or tp try: return issubclass(origin, ForeignKeyValidator) except TypeError: return False # --------- merge logic using cached info ---------
[docs] def merge(self, session: Session, load: bool = False) -> None: """ Merge all mapped ORM instances in this event into the given Session. Uses cached field info in _orm_fields to be fast. (Assumes @orm_event has already populated _orm_fields.) """ # optional safety: in case someone forgot the decorator if not type(self)._orm_fields: type(self)._init_orm_field_cache() register_orm_session( session, ) for field_name, kind in type(self)._orm_fields.items(): value = getattr(self, field_name) if value is None: continue if kind == "single": merged = session.merge(value, load=load) setattr(self, field_name, merged) elif kind == "list": merged_list = [session.merge(obj, load=load) for obj in value] setattr(self, field_name, merged_list)
# --------- the decorator that populates _orm_fields ---------
[docs] def orm_event(cls: Type[T]) -> Type[T]: """ Class decorator for SQLEvent subclasses. It precomputes the _orm_fields mapping at import time so merge() doesn’t need to do any reflection. """ if not issubclass(cls, ORMEvent): raise TypeError("@orm_event can only be used on SQLEvent subclasses") cls._init_orm_field_cache() return cls