""" NOS.nl Dutch news RSS client. Free RSS feeds from Netherlands public broadcaster. https://nos.nl/feeds 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 NOSProvider(NewsProvider): """NOS.nl RSS feed implementation.""" # Available NOS RSS feeds FEEDS: dict[str, str] = { # News "general": "https://feeds.nos.nl/nosnieuwsalgemeen", "domestic": "https://feeds.nos.nl/nosnieuwsbinnenland", "world": "https://feeds.nos.nl/nosnieuwsbuitenland", "politics": "https://feeds.nos.nl/nosnieuwspolitiek", "economy": "https://feeds.nos.nl/nosnieuwseconomie", "remarkable": "https://feeds.nos.nl/nosnieuwsopmerkelijk", "culture": "https://feeds.nos.nl/nosnieuwscultuurenmedia", "tech": "https://feeds.nos.nl/nosnieuwstech", # Sports "sports": "https://feeds.nos.nl/nossportalgemeen", "football": "https://feeds.nos.nl/nosvoetbal", "cycling": "https://feeds.nos.nl/nossportwielrennen", "skating": "https://feeds.nos.nl/nossportschaatsen", "tennis": "https://feeds.nos.nl/nossporttennis", "f1": "https://feeds.nos.nl/nossportformule1", } def __init__(self, timeout: int = 10): """ Initialize NOS 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 "nos" @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 (general, domestic, world, 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 # Extract image URL if available image_url = None if hasattr(entry, 'media_content') and entry.media_content: image_url = entry.media_content[0].get('url') elif hasattr(entry, 'enclosures') and entry.enclosures: for enc in entry.enclosures: if enc.get('type', '').startswith('image/'): image_url = enc.get('href') break 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"NOS feed request failed for '{category}': {e}") raise ValueError(f"Failed to fetch NOS feed: {e}") except Exception as e: logger.error(f"Failed to parse NOS feed '{category}': {e}") raise ValueError(f"Failed to parse NOS feed: {e}")