feat: add external API providers and central settings database
- Add central settings database client (system_settings on postgres-shared) - User-scoped settings with global fallback - API config storage with enabled/disabled toggle - Per-source category filtering for news - Add modular external API providers in src/apis/: - OpenMeteoProvider: weather with geocoding (free, no key) - NOSProvider: Dutch news RSS feeds - BBCProvider: English news RSS feeds - AggregatedNewsProvider: merges sources with category filtering - AlphaVantageProvider: financial quotes (API key from settings DB) - Add provider dependencies and lifecycle management - Add requirements-dev.txt with pip-audit for security auditing - Add MEMORY_REMEMBER_PLAN.md documenting volatile/document memory architecture 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+152
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
BBC News RSS client.
|
||||
|
||||
Free RSS feeds from BBC News.
|
||||
https://www.bbc.com/news/10628494 (RSS feed directory)
|
||||
|
||||
No API key required.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import feedparser
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from email.utils import parsedate_to_datetime
|
||||
from typing import Optional
|
||||
|
||||
from .base import NewsProvider, NewsItem, NewsFeed
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BBCProvider(NewsProvider):
|
||||
"""BBC News RSS feed implementation."""
|
||||
|
||||
# Available BBC RSS feeds
|
||||
FEEDS: dict[str, str] = {
|
||||
# News
|
||||
"top": "https://feeds.bbci.co.uk/news/rss.xml",
|
||||
"world": "https://feeds.bbci.co.uk/news/world/rss.xml",
|
||||
"uk": "https://feeds.bbci.co.uk/news/uk/rss.xml",
|
||||
"business": "https://feeds.bbci.co.uk/news/business/rss.xml",
|
||||
"politics": "https://feeds.bbci.co.uk/news/politics/rss.xml",
|
||||
"health": "https://feeds.bbci.co.uk/news/health/rss.xml",
|
||||
"education": "https://feeds.bbci.co.uk/news/education/rss.xml",
|
||||
"science": "https://feeds.bbci.co.uk/news/science_and_environment/rss.xml",
|
||||
"tech": "https://feeds.bbci.co.uk/news/technology/rss.xml",
|
||||
"entertainment": "https://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml",
|
||||
"asia": "https://feeds.bbci.co.uk/news/world/asia/rss.xml",
|
||||
"europe": "https://feeds.bbci.co.uk/news/world/europe/rss.xml",
|
||||
"africa": "https://feeds.bbci.co.uk/news/world/africa/rss.xml",
|
||||
# Sports
|
||||
"sports": "https://feeds.bbci.co.uk/sport/rss.xml",
|
||||
"football": "https://feeds.bbci.co.uk/sport/football/rss.xml",
|
||||
"cricket": "https://feeds.bbci.co.uk/sport/cricket/rss.xml",
|
||||
"tennis": "https://feeds.bbci.co.uk/sport/tennis/rss.xml",
|
||||
"rugby": "https://feeds.bbci.co.uk/sport/rugby-union/rss.xml",
|
||||
"f1": "https://feeds.bbci.co.uk/sport/motorsport/rss.xml",
|
||||
"golf": "https://feeds.bbci.co.uk/sport/golf/rss.xml",
|
||||
}
|
||||
|
||||
def __init__(self, timeout: int = 10):
|
||||
"""
|
||||
Initialize BBC RSS client.
|
||||
|
||||
Args:
|
||||
timeout: HTTP request timeout in seconds
|
||||
"""
|
||||
self.timeout = timeout
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
@property
|
||||
def client(self) -> httpx.AsyncClient:
|
||||
"""Lazy-initialize HTTP client."""
|
||||
if self._client is None or self._client.is_closed:
|
||||
self._client = httpx.AsyncClient(timeout=self.timeout)
|
||||
return self._client
|
||||
|
||||
async def close(self):
|
||||
"""Close HTTP client."""
|
||||
if self._client and not self._client.is_closed:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
@property
|
||||
def source_name(self) -> str:
|
||||
"""Provider name."""
|
||||
return "bbc"
|
||||
|
||||
@property
|
||||
def available_categories(self) -> list[str]:
|
||||
"""List of available category keys."""
|
||||
return list(self.FEEDS.keys())
|
||||
|
||||
async def get_feed(self, category: str, limit: int = 10) -> NewsFeed:
|
||||
"""
|
||||
Get news feed for a category.
|
||||
|
||||
Args:
|
||||
category: Feed category (top, world, uk, business, etc.)
|
||||
limit: Maximum number of items to return
|
||||
|
||||
Returns:
|
||||
NewsFeed with standardized news items
|
||||
|
||||
Raises:
|
||||
ValueError: If category is not available
|
||||
"""
|
||||
if category not in self.FEEDS:
|
||||
raise ValueError(
|
||||
f"Unknown category '{category}'. "
|
||||
f"Available: {', '.join(self.available_categories)}"
|
||||
)
|
||||
|
||||
feed_url = self.FEEDS[category]
|
||||
|
||||
try:
|
||||
response = await self.client.get(feed_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse RSS feed
|
||||
feed = feedparser.parse(response.text)
|
||||
|
||||
items = []
|
||||
for entry in feed.entries[:limit]:
|
||||
# Parse publication date
|
||||
published = None
|
||||
if hasattr(entry, 'published'):
|
||||
try:
|
||||
published = parsedate_to_datetime(entry.published)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
# BBC uses media:thumbnail for images
|
||||
image_url = None
|
||||
if hasattr(entry, 'media_thumbnail') and entry.media_thumbnail:
|
||||
image_url = entry.media_thumbnail[0].get('url')
|
||||
elif hasattr(entry, 'media_content') and entry.media_content:
|
||||
image_url = entry.media_content[0].get('url')
|
||||
|
||||
items.append(NewsItem(
|
||||
title=entry.get('title', 'No title'),
|
||||
description=entry.get('summary') or entry.get('description'),
|
||||
url=entry.get('link', ''),
|
||||
published=published,
|
||||
source=self.source_name,
|
||||
category=category,
|
||||
image_url=image_url
|
||||
))
|
||||
|
||||
return NewsFeed(
|
||||
source=self.source_name,
|
||||
category=category,
|
||||
items=items,
|
||||
fetched_at=datetime.now()
|
||||
)
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"BBC feed request failed for '{category}': {e}")
|
||||
raise ValueError(f"Failed to fetch BBC feed: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse BBC feed '{category}': {e}")
|
||||
raise ValueError(f"Failed to parse BBC feed: {e}")
|
||||
Reference in New Issue
Block a user