GitHub Actions commited on
Commit
e469aff
·
1 Parent(s): 1f5f31a

Sync from GitHub · 2026-10-20 08:32 UTC

Browse files
Files changed (2) hide show
  1. news/pipeline.py +13 -6
  2. 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 run_poll(since_days: int, triggered_by: str) -> dict:
 
 
 
 
 
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
- poll_id = poll.data[0]["id"]
 
 
 
 
 
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 run_poll
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
- try:
45
- result = run_poll(since_days=body.since_days, triggered_by=admin_uid)
46
- except Exception as e:
47
- raise HTTPException(status_code=502, detail=f"Poll failed: {e}")
48
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
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")