-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathingestion.py
More file actions
134 lines (114 loc) · 4.23 KB
/
Copy pathingestion.py
File metadata and controls
134 lines (114 loc) · 4.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""
FinQueryFlow - News API Route
Serves paginated news articles from PostgreSQL with filtering by ticker/source/date
"""
from fastapi import APIRouter, Query, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import psycopg2
from psycopg2.extras import RealDictCursor
import os
from datetime import datetime, timedelta
router = APIRouter()
PG_DSN = os.getenv("DATABASE_URL", "postgresql://finuser:finpass@localhost:5432/finqueryflow")
class Article(BaseModel):
id: str
ticker: str
source: str
title: str
url: Optional[str]
published_at: Optional[str]
ingested_at: Optional[str]
class NewsPage(BaseModel):
articles: List[Article]
total: int
page: int
pages: int
@router.get("/feed", response_model=NewsPage)
async def get_news_feed(
ticker: Optional[str] = Query(None, description="Filter by ticker symbol e.g. NVDA"),
source: Optional[str] = Query(None, description="Filter by source: newsapi | polygon | reddit"),
hours: int = Query(24, ge=1, le=168, description="Lookback window in hours"),
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
):
"""
Paginated news feed from PostgreSQL.
Supports filtering by ticker, source, and lookback window.
"""
offset = (page - 1) * limit
since = datetime.utcnow() - timedelta(hours=hours)
conditions = ["published_at > %s"]
params: list = [since]
if ticker:
conditions.append("ticker = %s")
params.append(ticker.upper())
if source:
conditions.append("source = %s")
params.append(source.lower())
where = " AND ".join(conditions)
try:
conn = psycopg2.connect(PG_DSN)
with conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(f"SELECT COUNT(*) as cnt FROM articles WHERE {where}", params)
total = cur.fetchone()["cnt"]
cur.execute(
f"""
SELECT id, ticker, source, title, url,
published_at::text, ingested_at::text
FROM articles WHERE {where}
ORDER BY published_at DESC
LIMIT %s OFFSET %s
""",
params + [limit, offset],
)
rows = cur.fetchall()
conn.close()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {e}")
import math
return NewsPage(
articles=[Article(**r) for r in rows],
total=total,
page=page,
pages=math.ceil(total / limit),
)
@router.get("/latest/{ticker}")
async def latest_news(ticker: str, limit: int = Query(10, ge=1, le=50)):
"""Get the N most recent articles for a ticker."""
try:
conn = psycopg2.connect(PG_DSN)
with conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
"""
SELECT id, ticker, source, title, url, published_at::text
FROM articles WHERE ticker = %s
ORDER BY published_at DESC LIMIT %s
""",
(ticker.upper(), limit),
)
rows = cur.fetchall()
conn.close()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {"ticker": ticker.upper(), "articles": rows, "count": len(rows)}
@router.get("/stats")
async def news_stats():
"""Summary statistics: article counts by ticker and source for last 24h."""
try:
conn = psycopg2.connect(PG_DSN)
with conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("""
SELECT ticker, source, COUNT(*) as count
FROM articles
WHERE published_at > NOW() - INTERVAL '24 hours'
GROUP BY ticker, source
ORDER BY count DESC
""")
rows = cur.fetchall()
cur.execute("SELECT COUNT(*) as total FROM articles")
total = cur.fetchone()["total"]
conn.close()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return {"breakdown": rows, "total_articles_all_time": total}