Spaces:
Running
Running
GitHub Actions commited on
Commit ·
e469aff
1
Parent(s): 1f5f31a
Sync from GitHub · 2026-10-20 08:32 UTC
Browse files- news/pipeline.py +13 -6
- routes/news.py +27 -8
news/pipeline.py
CHANGED
|
@@ -20,17 +20,24 @@ from news.windows import compute_expiry
|
|
| 20 |
from utils.supabase_client import get_supabase
|
| 21 |
|
| 22 |
|
| 23 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
db = get_supabase()
|
| 25 |
-
now = datetime.now(timezone.utc)
|
| 26 |
-
cutoff = now - timedelta(days=since_days)
|
| 27 |
-
|
| 28 |
poll = db.table("news_polls").insert({
|
| 29 |
"triggered_by": triggered_by,
|
| 30 |
"since_days": since_days,
|
| 31 |
-
"started_at": now.isoformat(),
|
| 32 |
}).execute()
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
counts = {"items_scraped": 0, "items_deduped_out": 0, "items_classified": 0, "items_flagged": 0}
|
| 36 |
error: str | None = None
|
|
|
|
| 20 |
from utils.supabase_client import get_supabase
|
| 21 |
|
| 22 |
|
| 23 |
+
def start_poll(since_days: int, triggered_by: str) -> str:
|
| 24 |
+
"""Records the poll row and hands back its id immediately — the scrape+classify
|
| 25 |
+
work for even a handful of sources takes minutes (Groq calls per item), which
|
| 26 |
+
is well past most gateway timeouts (HF Spaces included). The caller runs
|
| 27 |
+
execute_poll(poll_id, since_days) as a background task rather than awaiting
|
| 28 |
+
it inline, so the HTTP response never has to wait on it."""
|
| 29 |
db = get_supabase()
|
|
|
|
|
|
|
|
|
|
| 30 |
poll = db.table("news_polls").insert({
|
| 31 |
"triggered_by": triggered_by,
|
| 32 |
"since_days": since_days,
|
| 33 |
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
| 34 |
}).execute()
|
| 35 |
+
return poll.data[0]["id"]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def execute_poll(poll_id: str, since_days: int) -> dict:
|
| 39 |
+
db = get_supabase()
|
| 40 |
+
cutoff = datetime.now(timezone.utc) - timedelta(days=since_days)
|
| 41 |
|
| 42 |
counts = {"items_scraped": 0, "items_deduped_out": 0, "items_classified": 0, "items_flagged": 0}
|
| 43 |
error: str | None = None
|
routes/news.py
CHANGED
|
@@ -3,18 +3,24 @@ Business News Routes (ADR-0015)
|
|
| 3 |
──────────────────────────────────────────────────────────────────────────────
|
| 4 |
Routes:
|
| 5 |
POST /api/news/poll → admin-triggered scrape+classify run (max 30 days back)
|
|
|
|
| 6 |
GET /api/news/items → classified, unexpired items (optionally filtered by business type)
|
| 7 |
GET /api/news/admin/review → admin: items flagged incomplete, awaiting a human fix
|
| 8 |
PATCH /api/news/admin/review/{id} → admin: manually complete or reject a flagged item
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
from datetime import datetime, timezone
|
| 12 |
|
| 13 |
-
from fastapi import APIRouter, HTTPException, Depends
|
| 14 |
from pydantic import BaseModel, Field
|
| 15 |
from firebase_admin import firestore
|
| 16 |
from auth import verify_firebase_token
|
| 17 |
-
from news.pipeline import
|
| 18 |
from news.classify import IMPACTS, CATEGORIES
|
| 19 |
from news.business_types import valid_subset
|
| 20 |
from utils.supabase_client import get_supabase
|
|
@@ -39,13 +45,26 @@ class PollRequest(BaseModel):
|
|
| 39 |
|
| 40 |
|
| 41 |
@router.post("/poll")
|
| 42 |
-
def poll_news(body: PollRequest, user: dict = Depends(verify_firebase_token)):
|
| 43 |
admin_uid = _verify_admin(user)
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
@router.get("/items")
|
|
|
|
| 3 |
──────────────────────────────────────────────────────────────────────────────
|
| 4 |
Routes:
|
| 5 |
POST /api/news/poll → admin-triggered scrape+classify run (max 30 days back)
|
| 6 |
+
GET /api/news/admin/polls → admin: recent poll runs and their outcome
|
| 7 |
GET /api/news/items → classified, unexpired items (optionally filtered by business type)
|
| 8 |
GET /api/news/admin/review → admin: items flagged incomplete, awaiting a human fix
|
| 9 |
PATCH /api/news/admin/review/{id} → admin: manually complete or reject a flagged item
|
| 10 |
+
|
| 11 |
+
Polling runs as a background task (see news/pipeline.py) because scraping every
|
| 12 |
+
source and classifying each new item through Groq takes minutes — far past any
|
| 13 |
+
gateway's request timeout. POST /poll returns as soon as the run is recorded;
|
| 14 |
+
the frontend watches GET /admin/polls for it to finish.
|
| 15 |
"""
|
| 16 |
|
| 17 |
from datetime import datetime, timezone
|
| 18 |
|
| 19 |
+
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
|
| 20 |
from pydantic import BaseModel, Field
|
| 21 |
from firebase_admin import firestore
|
| 22 |
from auth import verify_firebase_token
|
| 23 |
+
from news.pipeline import start_poll, execute_poll
|
| 24 |
from news.classify import IMPACTS, CATEGORIES
|
| 25 |
from news.business_types import valid_subset
|
| 26 |
from utils.supabase_client import get_supabase
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
@router.post("/poll")
|
| 48 |
+
def poll_news(body: PollRequest, background_tasks: BackgroundTasks, user: dict = Depends(verify_firebase_token)):
|
| 49 |
admin_uid = _verify_admin(user)
|
| 50 |
+
poll_id = start_poll(since_days=body.since_days, triggered_by=admin_uid)
|
| 51 |
+
background_tasks.add_task(execute_poll, poll_id, body.since_days)
|
| 52 |
+
return {"poll_id": poll_id, "started": True}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@router.get("/admin/polls")
|
| 56 |
+
def list_polls(user: dict = Depends(verify_firebase_token)):
|
| 57 |
+
_verify_admin(user)
|
| 58 |
+
db = get_supabase()
|
| 59 |
+
polls = (
|
| 60 |
+
db.table("news_polls")
|
| 61 |
+
.select("id, triggered_by, since_days, started_at, finished_at, items_scraped, items_deduped_out, items_classified, items_flagged, error")
|
| 62 |
+
.order("started_at", desc=True)
|
| 63 |
+
.limit(10)
|
| 64 |
+
.execute()
|
| 65 |
+
.data
|
| 66 |
+
)
|
| 67 |
+
return {"polls": polls}
|
| 68 |
|
| 69 |
|
| 70 |
@router.get("/items")
|