from __future__ import annotations from datetime import datetime from sqlalchemy import ( Boolean, DateTime, Float, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func, ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): pass class Tracker(Base): __tablename__ = "trackers" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) canonical_url: Mapped[str] = mapped_column(String(512), unique=True, nullable=False) scheme: Mapped[str] = mapped_column(String(16), nullable=False) hostname: Mapped[str] = mapped_column(String(255), nullable=False, index=True) port: Mapped[int] = mapped_column(Integer, nullable=False) path: Mapped[str] = mapped_column(String(512), nullable=False, default="/announce") current_status: Mapped[str] = mapped_column( String(32), nullable=False, default="unknown", index=True ) first_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) last_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) last_checked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) next_check_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, index=True ) announced_interval_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True) last_latency_ms: Mapped[float | None] = mapped_column(Float, nullable=True) supports_ipv4: Mapped[bool | None] = mapped_column(Boolean, nullable=True) supports_ipv6: Mapped[str | None] = mapped_column(String(32), nullable=True) # supports_ipv6 stores: true / false / not_tested / unknown as string for flexibility terminal_cname: Mapped[str | None] = mapped_column(String(255), nullable=True) infrastructure_fingerprint: Mapped[str | None] = mapped_column( String(128), nullable=True, index=True ) asn: Mapped[int | None] = mapped_column(Integer, nullable=True) network_name: Mapped[str | None] = mapped_column(String(255), nullable=True) country_code: Mapped[str | None] = mapped_column(String(8), nullable=True) consecutive_failures: Mapped[int] = mapped_column(Integer, nullable=False, default=0) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False, ) sources: Mapped[list[TrackerSource]] = relationship(back_populates="tracker") probe_results: Mapped[list[ProbeResult]] = relationship(back_populates="tracker") dns_records: Mapped[list[DnsRecord]] = relationship(back_populates="tracker") class TrackerSource(Base): __tablename__ = "tracker_sources" __table_args__ = (UniqueConstraint("tracker_id", "source_name", name="uq_tracker_source"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tracker_id: Mapped[int] = mapped_column( ForeignKey("trackers.id", ondelete="CASCADE"), nullable=False, index=True ) source_name: Mapped[str] = mapped_column(String(64), nullable=False) source_url: Mapped[str] = mapped_column(String(512), nullable=False) is_best_at_source: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_stable_at_source: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_live_at_source: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) first_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) last_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) tracker: Mapped[Tracker] = relationship(back_populates="sources") class ProbeResult(Base): __tablename__ = "probe_results" __table_args__ = (Index("ix_probe_results_checked_at", "checked_at"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tracker_id: Mapped[int] = mapped_column( ForeignKey("trackers.id", ondelete="CASCADE"), nullable=False, index=True ) checked_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False) latency_ms: Mapped[float | None] = mapped_column(Float, nullable=True) ip_address: Mapped[str | None] = mapped_column(String(64), nullable=True) ip_family: Mapped[str | None] = mapped_column(String(8), nullable=True) response_valid: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) tracker_interval_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True) seeders: Mapped[int | None] = mapped_column(Integer, nullable=True) leechers: Mapped[int | None] = mapped_column(Integer, nullable=True) error_kind: Mapped[str | None] = mapped_column(String(64), nullable=True) error_detail: Mapped[str | None] = mapped_column(String(512), nullable=True) tracker: Mapped[Tracker] = relationship(back_populates="probe_results") class DnsRecord(Base): __tablename__ = "dns_records" __table_args__ = (UniqueConstraint("tracker_id", "record_type", "value", name="uq_dns_record"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tracker_id: Mapped[int] = mapped_column( ForeignKey("trackers.id", ondelete="CASCADE"), nullable=False, index=True ) record_type: Mapped[str] = mapped_column(String(16), nullable=False) value: Mapped[str] = mapped_column(String(512), nullable=False) first_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) last_seen_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) tracker: Mapped[Tracker] = relationship(back_populates="dns_records") class SourceImportRun(Base): __tablename__ = "source_import_runs" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) source_name: Mapped[str] = mapped_column(String(64), nullable=False, index=True) started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) status: Mapped[str] = mapped_column(String(32), nullable=False, default="running") received_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) valid_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) new_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) unsupported_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) error_message: Mapped[str | None] = mapped_column(String(512), nullable=True) etag: Mapped[str | None] = mapped_column(String(255), nullable=True) last_modified: Mapped[str | None] = mapped_column(String(255), nullable=True) class CollectorCache(Base): """HTTP cache state per source feed URL.""" __tablename__ = "collector_cache" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) source_name: Mapped[str] = mapped_column(String(64), nullable=False) feed_url: Mapped[str] = mapped_column(String(512), nullable=False, unique=True) etag: Mapped[str | None] = mapped_column(String(255), nullable=True) last_modified: Mapped[str | None] = mapped_column(String(255), nullable=True) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class AppState(Base): """Key/value operational state (bootstrap notes, etc.).""" __tablename__ = "app_state" key: Mapped[str] = mapped_column(String(64), primary_key=True) value: Mapped[str] = mapped_column(Text, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False )