"""news_sources 테이블 ORM""" from datetime import datetime from sqlalchemy import Boolean, DateTime, Enum, Integer, String, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from core.database import Base class NewsSource(Base): __tablename__ = "news_sources" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(100), nullable=False) country: Mapped[str | None] = mapped_column(String(10)) feed_url: Mapped[str] = mapped_column(Text, nullable=False) feed_type: Mapped[str] = mapped_column(String(20), default="rss") category: Mapped[str | None] = mapped_column(String(50)) language: Mapped[str | None] = mapped_column(String(10)) enabled: Mapped[bool] = mapped_column(Boolean, default=True) last_fetched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=datetime.now ) # ── A-3 (plan crawl-24x7-1) 레지스트리 증축 — migration 319 ── # fetch_method: rss / rss+page / sitemap+page / page / api / signal-only fetch_method: Mapped[str] = mapped_column(String(20), default="rss") # fulltext_policy: none(현행) / page(기사 페이지 fetch 후 4-tier 승격) / feed-full(피드 본문이 전문) fulltext_policy: Mapped[str] = mapped_column(String(20), default="none") # NULL=공개, 값=구독 세션 키 (B-3 Playwright 어댑터 슬롯) auth_profile: Mapped[str | None] = mapped_column(String(50)) # 소스별 차등 폴링 (NULL=전역 6h 사이클) poll_interval_minutes: Mapped[int | None] = mapped_column(Integer) # 조건부 GET 워터마크 — 서버가 준 값 그대로 저장·재전송 (A-1) etag: Mapped[str | None] = mapped_column(Text) last_modified: Mapped[str | None] = mapped_column(Text) # CDN ETag 회전 대비 콘텐츠 해시 변경감지 병행 (A-1) feed_content_hash: Mapped[str | None] = mapped_column(String(64)) # 추출 실패 잦은 소스의 site-specific CSS selector (A-2) selector_override: Mapped[dict | None] = mapped_column(JSONB) # rdf / table-strip / gn-redirect / skip-video 등 파서 특이 케이스 (B-5) parser_quirk: Mapped[str | None] = mapped_column(String(30)) # 채널 — 'news'(다이제스트/브리핑 대상) / 'crawl'(도메인 재료, 0-5 (a)) — migration 324. # documents.source_channel 로 전파, crawl 채널은 embed/chunk 30일 게이트 미적용. # documents 와 동일 PG enum 재사용 (Document 모델과 값 목록 동기 유지). source_channel: Mapped[str] = mapped_column( Enum("law_monitor", "devonagent", "email", "web_clip", "tksafety", "inbox_route", "manual", "drive_sync", "news", "memo", "voice", "hermes", "crawl", name="source_channel"), default="news", ) # ── 안전 자료실 분류 축 (plan safety-library-1 A-2, migrations 352~355) ── # 자료유형 기본값 — documents.material_type 으로 ingest 시점 전파 (NULL=비대상). # jurisdiction 은 별도 컬럼 없이 country 전파, 단 paper 는 코드에서 NULL 강제. material_type: Mapped[str | None] = mapped_column(Text) # extract_meta.license 주입용 — kogl/ogl/public_domain/proprietary/unknown. # 미확정 = 보수적(unknown + redistribute=false), 근거 확보 시 완화. license_scheme: Mapped[str | None] = mapped_column(Text) license_redistribute: Mapped[bool | None] = mapped_column(Boolean)