daily-briefing/server/services/news_service.py

154 lines
4 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncpg
from typing import Any, Dict, List, Optional
_pool: Optional[asyncpg.Pool] = None
async def init_pool(
host: str,
port: int,
dbname: str,
user: str,
password: str,
) -> None:
"""Initialise the global asyncpg connection pool.
Call once at application startup.
"""
global _pool
_pool = await asyncpg.create_pool(
host=host,
port=port,
database=dbname,
user=user,
password=password,
min_size=1,
max_size=5,
)
async def close_pool() -> None:
"""Close the global asyncpg connection pool.
Call once at application shutdown.
"""
global _pool
if _pool is not None:
await _pool.close()
_pool = None
def _row_to_dict(row: asyncpg.Record) -> Dict[str, Any]:
"""Convert an asyncpg Record to a plain dictionary with JSON-safe values."""
d: Dict[str, Any] = dict(row)
if "published_at" in d and d["published_at"] is not None:
d["published_at"] = d["published_at"].isoformat()
return d
async def get_news(
limit: int = 20,
offset: int = 0,
category: Optional[str] = None,
max_age_hours: int = 48,
) -> List[Dict[str, Any]]:
"""Fetch recent news articles from the market_news table.
Args:
limit: Maximum number of rows to return.
offset: Number of rows to skip (for pagination).
category: Optional category filter (exact match).
max_age_hours: Only return articles published within this many hours.
Returns:
List of news article dictionaries.
"""
if _pool is None:
raise RuntimeError("Database pool is not initialised. Call init_pool() first.")
params: List[Any] = []
param_idx = 1
base_query = (
"SELECT id, source, title, url, category, published_at "
"FROM market_news "
f"WHERE published_at > NOW() - INTERVAL '{int(max_age_hours)} hours'"
)
if category is not None:
base_query += f" AND category = ${param_idx}"
params.append(category)
param_idx += 1
base_query += f" ORDER BY published_at DESC LIMIT ${param_idx} OFFSET ${param_idx + 1}"
params.append(limit)
params.append(offset)
async with _pool.acquire() as conn:
rows = await conn.fetch(base_query, *params)
return [_row_to_dict(row) for row in rows]
async def get_news_count(
max_age_hours: int = 48,
category: Optional[str] = None,
) -> int:
"""Return the total count of recent news articles.
Args:
max_age_hours: Only count articles published within this many hours.
category: Optional category filter.
Returns:
Integer count.
"""
if _pool is None:
raise RuntimeError("Database pool is not initialised. Call init_pool() first.")
params: List[Any] = []
param_idx = 1
query = (
"SELECT COUNT(*) AS cnt "
"FROM market_news "
f"WHERE published_at > NOW() - INTERVAL '{int(max_age_hours)} hours'"
)
if category is not None:
query += f" AND category = ${param_idx}"
params.append(category)
async with _pool.acquire() as conn:
row = await conn.fetchrow(query, *params)
return int(row["cnt"]) if row else 0
async def get_categories(max_age_hours: int = 48) -> List[str]:
"""Return distinct categories from recent news articles.
Args:
max_age_hours: Only consider articles published within this many hours.
Returns:
Sorted list of category strings.
"""
if _pool is None:
raise RuntimeError("Database pool is not initialised. Call init_pool() first.")
query = (
"SELECT DISTINCT category "
"FROM market_news "
f"WHERE published_at > NOW() - INTERVAL '{int(max_age_hours)} hours' "
"AND category IS NOT NULL "
"ORDER BY category"
)
async with _pool.acquire() as conn:
rows = await conn.fetch(query)
return [row["category"] for row in rows]