from datetime import datetime from typing import Any, Optional from uuid import uuid4 from sqlalchemy import DateTime, ForeignKey, Integer, String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.types import Uuid from .base import Base TASK_STATUSES = ("pending", "running", "success", "partial", "failed", "stale", "cancelled") class Task(Base): __tablename__ = "tasks" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=lambda: str(uuid4()) ) type: Mapped[str] = mapped_column(String(100), nullable=False) status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") target_scope: Mapped[dict] = mapped_column(JSONB, nullable=False) params: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) result: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) created_by: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) heartbeat_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) events: Mapped[list["TaskEvent"]] = relationship( "TaskEvent", back_populates="task", cascade="all, delete-orphan" ) device_results: Mapped[list["TaskResult"]] = relationship( "TaskResult", back_populates="task", cascade="all, delete-orphan" ) class TaskEvent(Base): __tablename__ = "task_events" id: Mapped[int] = mapped_column(Integer, primary_key=True) task_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, index=True ) message: Mapped[str] = mapped_column(Text, nullable=False) level: Mapped[str] = mapped_column(String(10), nullable=False, default="info") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) task: Mapped["Task"] = relationship("Task", back_populates="events") class TaskResult(Base): __tablename__ = "task_results" id: Mapped[int] = mapped_column(Integer, primary_key=True) task_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, index=True ) device_id: Mapped[int] = mapped_column( Integer, ForeignKey("devices.id", ondelete="CASCADE"), nullable=False, index=True ) status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") stdout: Mapped[Optional[str]] = mapped_column(Text, nullable=True) stderr: Mapped[Optional[str]] = mapped_column(Text, nullable=True) exit_code: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) data: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True) duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) executed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) task: Mapped["Task"] = relationship("Task", back_populates="device_results")