"""ORM models and lifecycle states shared by every component. Status machines --------------- proposal: proposed -> approved -> running -> done | failed proposed -> rejected mail: (inbound) classified: approval|rejection|question|other|needs_review (outbound draft) none -> drafted -> approved -> sent | skipped content: draft -> approved -> published """ from __future__ import annotations import enum from datetime import UTC, datetime from sqlalchemy import JSON, DateTime, Enum, Float, ForeignKey, Integer, String, Text from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column def utcnow() -> datetime: return datetime.now(UTC) class Base(DeclarativeBase): pass # --- proposals --------------------------------------------------------------- class ProposalStatus(enum.StrEnum): PROPOSED = "proposed" APPROVED = "approved" REJECTED = "rejected" RUNNING = "running" DONE = "done" FAILED = "failed" class ProposalSource(enum.StrEnum): SCANNER = "scanner" MANUAL = "manual" class Proposal(Base): __tablename__ = "proposals" id: Mapped[int] = mapped_column(Integer, primary_key=True) source: Mapped[ProposalSource] = mapped_column( Enum(ProposalSource), default=ProposalSource.SCANNER ) kind: Mapped[str] = mapped_column(String(64)) # affiliate|partnership|content_gap|other title: Mapped[str] = mapped_column(String(512)) url: Mapped[str | None] = mapped_column(String(1024), nullable=True) detail: Mapped[dict] = mapped_column(JSON, default=dict) risk_score: Mapped[float] = mapped_column(Float, default=0.0) status: Mapped[ProposalStatus] = mapped_column( Enum(ProposalStatus), default=ProposalStatus.PROPOSED, index=True ) attempts: Mapped[int] = mapped_column(Integer, default=0) dedupe_key: Mapped[str | None] = mapped_column(String(256), unique=True, nullable=True) approved_by: Mapped[str | None] = mapped_column(String(128), nullable=True) approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utcnow, onupdate=utcnow ) # --- audit ------------------------------------------------------------------- class AuditEntry(Base): __tablename__ = "audit_log" id: Mapped[int] = mapped_column(Integer, primary_key=True) ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True) actor: Mapped[str] = mapped_column(String(128)) # human username | auto:policy | daemon: proposal_id: Mapped[int | None] = mapped_column( ForeignKey("proposals.id", ondelete="SET NULL"), nullable=True, index=True ) action: Mapped[str] = mapped_column(String(64)) # approve|reject|execute|classify|... detail: Mapped[dict] = mapped_column(JSON, default=dict) result: Mapped[str] = mapped_column(String(32), default="ok") # ok | error def audit( sess, actor: str, action: str, proposal_id: int | None = None, detail: dict | None = None, result: str = "ok", ) -> AuditEntry: """Record one audited action. Every component writes through this helper.""" entry = AuditEntry( actor=actor, action=action, proposal_id=proposal_id, detail=detail or {}, result=result, ) sess.add(entry) return entry # --- mail -------------------------------------------------------------------- class MailDirection(enum.StrEnum): INBOUND = "in" OUTBOUND = "out" class MailClassification(enum.StrEnum): APPROVAL = "approval" REJECTION = "rejection" QUESTION = "question" OTHER = "other" NEEDS_REVIEW = "needs_review" class DraftStatus(enum.StrEnum): NONE = "none" DRAFTED = "drafted" APPROVED = "approved" # human approved the draft; SMTP may send it SENT = "sent" SKIPPED = "skipped" class MailMessage(Base): __tablename__ = "mail_messages" id: Mapped[int] = mapped_column(Integer, primary_key=True) direction: Mapped[MailDirection] = mapped_column(Enum(MailDirection)) message_id: Mapped[str | None] = mapped_column(String(998), nullable=True, index=True) in_reply_to: Mapped[str | None] = mapped_column(String(998), nullable=True) from_addr: Mapped[str | None] = mapped_column(String(320), nullable=True) to_addr: Mapped[str | None] = mapped_column(String(320), nullable=True) subject: Mapped[str | None] = mapped_column(String(998), nullable=True) body: Mapped[str | None] = mapped_column(Text, nullable=True) classification: Mapped[MailClassification | None] = mapped_column( Enum(MailClassification), nullable=True ) confidence: Mapped[float] = mapped_column(Float, default=0.0) proposal_id: Mapped[int | None] = mapped_column( ForeignKey("proposals.id", ondelete="SET NULL"), nullable=True, index=True ) draft_status: Mapped[DraftStatus] = mapped_column( Enum(DraftStatus), default=DraftStatus.NONE ) draft_reply: Mapped[str | None] = mapped_column(Text, nullable=True) processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # --- content ----------------------------------------------------------------- class ContentStatus(enum.StrEnum): DRAFT = "draft" APPROVED = "approved" PUBLISHED = "published" class ContentItem(Base): __tablename__ = "content_items" id: Mapped[int] = mapped_column(Integer, primary_key=True) theme: Mapped[str] = mapped_column(String(128)) channel: Mapped[str] = mapped_column(String(32)) # nostr|facebook|instagram caption: Mapped[str] = mapped_column(Text) media_path: Mapped[str | None] = mapped_column(String(1024), nullable=True) link: Mapped[str | None] = mapped_column(String(1024), nullable=True) status: Mapped[ContentStatus] = mapped_column( Enum(ContentStatus), default=ContentStatus.DRAFT, index=True ) scheduled_for: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) post_ref: Mapped[str | None] = mapped_column(String(512), nullable=True) error: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) # --- daemon runs (monitor input) --------------------------------------------- class DaemonRun(Base): __tablename__ = "daemon_runs" id: Mapped[int] = mapped_column(Integer, primary_key=True) # scanner | executor | maild | publisher | monitor daemon: Mapped[str] = mapped_column(String(32), index=True) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) status: Mapped[str] = mapped_column(String(16), default="ok") # ok | error detail: Mapped[dict] = mapped_column(JSON, default=dict)