""" 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}")