Supervised source candidates: stage, list, promote, reject
- New source_candidates staging table (status suggested/quarantined/rejected/ promoted, preview_json snapshot) so untrusted/suggested feeds stay out of the real ingestion path until reviewed. - sources.py: save_candidate (re-preview never revives a curator's rejection), list_candidates, reject_candidate, promote_candidate (copies into sources, inactive by default — active on approval; never automatic). - CLI: suggest-source / list-candidates / promote-candidate / reject-candidate. - API: read-only GET /api/candidates (writes stay CLI-only — no unauthenticated public write surface yet). - Fix deprecated ElementTree truth-value test in _parse_rss. - Tests: candidate lifecycle (save/list/promote/reject, status preservation, name derivation) — 51 total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ so the API and CLI always read the same file.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
@@ -125,6 +126,19 @@ class RejectedExample(BaseModel):
|
||||
reason: str
|
||||
|
||||
|
||||
class Candidate(BaseModel):
|
||||
id: int
|
||||
feed_url: str
|
||||
homepage_url: str | None = None
|
||||
name: str | None = None
|
||||
status: str
|
||||
preview: dict | None = None
|
||||
notes: str | None = None
|
||||
last_previewed_at: str | None = None
|
||||
created_at: str | None = None
|
||||
updated_at: str | None = None
|
||||
|
||||
|
||||
class SourcePreview(BaseModel):
|
||||
url: str
|
||||
sampled: int
|
||||
@@ -251,6 +265,20 @@ def create_app() -> FastAPI:
|
||||
with get_conn() as conn:
|
||||
return queries.available_dates(conn, limit=limit)
|
||||
|
||||
@app.get("/api/candidates", response_model=list[Candidate])
|
||||
def candidates(status: str | None = Query(None)) -> list[Candidate]:
|
||||
from .sources import list_candidates
|
||||
|
||||
with get_conn() as conn:
|
||||
rows = list_candidates(conn, status=status)
|
||||
out = []
|
||||
for r in rows:
|
||||
d = dict(r)
|
||||
pj = d.pop("preview_json", None)
|
||||
d["preview"] = json.loads(pj) if pj else None
|
||||
out.append(Candidate(**d))
|
||||
return out
|
||||
|
||||
@app.get("/api/source-preview", response_model=SourcePreview)
|
||||
def source_preview(
|
||||
url: str = Query(..., max_length=2048),
|
||||
|
||||
Reference in New Issue
Block a user