from sqlalchemy import Column, Integer, DateTime, Boolean, String, JSON, func, inspect from sqlalchemy.orm import declarative_mixin, declarative_base, NO_VALUE Base = declarative_base() def _safe_get_loaded_attr(obj, name): try: st = inspect(obj) attr = st.attrs.get(name) if attr is not None: val = attr.loaded_value return None if val is NO_VALUE else val if name in st.dict: return st.dict.get(name) return None except Exception: return None @declarative_mixin class CRUDMixin: id = Column(Integer, primary_key=True) created_at = Column(DateTime, default=func.now(), nullable=False) updated_at = Column(DateTime, default=func.now(), nullable=False, onupdate=func.now()) def as_dict(self, fields: list[str] | None = None): """ Serialize the instance. - If 'fields' (possibly dotted) is provided, emit exactly those keys. - Else, if '__crudkit_projection__' is set on the instance, emit those keys. - Else, fall back to all mapped columns on this class hierarchy. Always includes 'id' when present unless explicitly excluded. """ if fields is None: fields = getattr(self, "__crudkit_projection__", None) if fields: out = {} if "id" not in fields and hasattr(self, "id"): out["id"] = getattr(self, "id") for f in fields: cur = self for part in f.split("."): if cur is None: break cur = getattr(cur, part, None) out[f] = cur return out result = {} for cls in self.__class__.__mro__: if hasattr(cls, "__table__"): for column in cls.__table__.columns: name = column.name result[name] = getattr(self, name) return result class Version(Base): __tablename__ = "versions" id = Column(Integer, primary_key=True) model_name = Column(String, nullable=False) object_id = Column(Integer, nullable=False) change_type = Column(String, nullable=False) data = Column(JSON, nullable=True) timestamp = Column(DateTime, default=func.now()) actor = Column(String, nullable=True) meta = Column('metadata', JSON, nullable=True)