"""Download model - individual download jobs.""" from datetime import datetime from typing import Optional from sqlalchemy import String, Integer, Text, ForeignKey, BigInteger from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base, TimestampMixin, format_datetime class DownloadStatus: """Download status constants.""" PENDING = "pending" RUNNING = "running" COMPLETED = "completed" FAILED = "failed" SKIPPED = "skipped" class ErrorType: """Error type constants matching legacy project.""" AUTH_ERROR = "auth_error" RATE_LIMITED = "rate_limited" NOT_FOUND = "not_found" ACCESS_DENIED = "access_denied" NETWORK_ERROR = "network_error" NO_NEW_CONTENT = "no_new_content" TIMEOUT = "timeout" HTTP_ERROR = "http_error" UNSUPPORTED_URL = "unsupported_url" UNKNOWN_ERROR = "unknown_error" class Download(Base, TimestampMixin): """A download represents a single download job for a source.""" __tablename__ = "downloads" id: Mapped[int] = mapped_column(primary_key=True) source_id: Mapped[Optional[int]] = mapped_column( ForeignKey("sources.id", ondelete="SET NULL"), nullable=True, index=True ) url: Mapped[str] = mapped_column(Text, nullable=False) # Status status: Mapped[str] = mapped_column( String(20), default=DownloadStatus.PENDING, index=True ) error_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Results file_count: Mapped[int] = mapped_column(Integer, default=0) total_size: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True) # Timing started_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) # Flexible metadata metadata_: Mapped[dict] = mapped_column("metadata", JSONB, default=dict) # Relationships source: Mapped[Optional["Source"]] = relationship(back_populates="downloads") content_items: Mapped[list["ContentItem"]] = relationship(back_populates="download") def __repr__(self) -> str: return f"" def to_dict(self) -> dict: """Convert to dictionary for API responses.""" return { "id": self.id, "source_id": self.source_id, "url": self.url, "status": self.status, "error_type": self.error_type, "error_message": self.error_message, "file_count": self.file_count, "total_size": self.total_size, "started_at": format_datetime(self.started_at), "completed_at": format_datetime(self.completed_at), "metadata": self.metadata_, "created_at": format_datetime(self.created_at), } # Import for type hints from typing import TYPE_CHECKING if TYPE_CHECKING: from app.models.source import Source from app.models.content import ContentItem