Source code for pydotorg.core.feeds.service

"""RSS 2.0 and Atom 1.0 feed generation services."""

from __future__ import annotations

import datetime
import html
from typing import TYPE_CHECKING
from xml.etree import ElementTree as ET

if TYPE_CHECKING:
    from collections.abc import Sequence

    from pydotorg.domains.events.models import Event


def _escape_cdata(text: str) -> str:
    """Escape text for XML CDATA sections."""
    if not text:
        return ""
    return html.escape(text, quote=False)


def _format_rfc822(dt: datetime.datetime) -> str:
    """Format datetime as RFC 822 for RSS."""
    return dt.strftime("%a, %d %b %Y %H:%M:%S %z") if dt.tzinfo else dt.strftime("%a, %d %b %Y %H:%M:%S +0000")


def _format_rfc3339(dt: datetime.datetime) -> str:
    """Format datetime as RFC 3339 for Atom."""
    if dt.tzinfo:
        return dt.isoformat()
    return dt.replace(tzinfo=datetime.UTC).isoformat()


[docs] class RSSFeedService: """Service for generating RSS 2.0 feeds for events.""" VERSION = "2.0"
[docs] def __init__( self, title: str = "Python Events", link: str = "https://www.python.org/events/", description: str = "Upcoming Python community events", language: str = "en-us", ) -> None: self.title = title self.link = link self.description = description self.language = language
def _create_item( self, event: Event, base_url: str = "https://www.python.org", ) -> ET.Element: """Create an RSS item element for an event.""" item = ET.Element("item") title = ET.SubElement(item, "title") title.text = event.title link = ET.SubElement(item, "link") link.text = f"{base_url}/events/{event.slug}/" guid = ET.SubElement(item, "guid") guid.set("isPermaLink", "true") guid.text = f"{base_url}/events/{event.slug}/" if event.description: description = ET.SubElement(item, "description") description.text = _escape_cdata(event.description[:500]) if event.occurrences: first_occurrence = min(event.occurrences, key=lambda o: o.dt_start) pub_date = ET.SubElement(item, "pubDate") pub_date.text = _format_rfc822(first_occurrence.dt_start) if event.categories: for cat in event.categories: category = ET.SubElement(item, "category") category.text = cat.name if event.venue: source = ET.SubElement(item, "source") source.set("url", f"{base_url}/events/") location_parts = [event.venue.name] if event.venue.address: location_parts.append(event.venue.address) source.text = ", ".join(location_parts) return item
[docs] def generate_feed( self, events: Sequence[Event], base_url: str = "https://www.python.org", feed_url: str | None = None, ) -> str: """Generate RSS 2.0 feed XML for events. Args: events: Sequence of Event objects to include in feed base_url: Base URL for event links feed_url: URL of the feed itself (for self-reference) Returns: RSS 2.0 XML string """ rss = ET.Element("rss") rss.set("version", self.VERSION) rss.set("xmlns:atom", "http://www.w3.org/2005/Atom") channel = ET.SubElement(rss, "channel") title = ET.SubElement(channel, "title") title.text = self.title link = ET.SubElement(channel, "link") link.text = self.link description = ET.SubElement(channel, "description") description.text = self.description language = ET.SubElement(channel, "language") language.text = self.language last_build = ET.SubElement(channel, "lastBuildDate") last_build.text = _format_rfc822(datetime.datetime.now(datetime.UTC)) generator = ET.SubElement(channel, "generator") generator.text = "Python.org Litestar" if feed_url: atom_link = ET.SubElement(channel, "{http://www.w3.org/2005/Atom}link") atom_link.set("href", feed_url) atom_link.set("rel", "self") atom_link.set("type", "application/rss+xml") for event in events: channel.append(self._create_item(event, base_url)) ET.register_namespace("atom", "http://www.w3.org/2005/Atom") return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(rss, encoding="unicode")
[docs] class AtomFeedService: """Service for generating Atom 1.0 feeds for events.""" NAMESPACE = "http://www.w3.org/2005/Atom"
[docs] def __init__( self, title: str = "Python Events", subtitle: str = "Upcoming Python community events", author_name: str = "Python Software Foundation", author_email: str = "webmaster@python.org", ) -> None: self.title = title self.subtitle = subtitle self.author_name = author_name self.author_email = author_email
def _create_entry( self, event: Event, base_url: str = "https://www.python.org", ) -> ET.Element: """Create an Atom entry element for an event.""" entry = ET.Element("entry") title = ET.SubElement(entry, "title") title.text = event.title link = ET.SubElement(entry, "link") link.set("href", f"{base_url}/events/{event.slug}/") link.set("rel", "alternate") link.set("type", "text/html") entry_id = ET.SubElement(entry, "id") entry_id.text = f"tag:python.org,2025:events/{event.id}" if event.occurrences: first_occurrence = min(event.occurrences, key=lambda o: o.dt_start) updated = ET.SubElement(entry, "updated") updated.text = _format_rfc3339(first_occurrence.dt_start) published = ET.SubElement(entry, "published") published.text = _format_rfc3339(first_occurrence.dt_start) if event.description: summary = ET.SubElement(entry, "summary") summary.set("type", "html") summary.text = _escape_cdata(event.description[:500]) content = ET.SubElement(entry, "content") content.set("type", "html") content.text = _escape_cdata(event.description) if event.categories: for cat in event.categories: category = ET.SubElement(entry, "category") category.set("term", cat.slug) category.set("label", cat.name) if event.venue: location_parts = [event.venue.name] if event.venue.address: location_parts.append(event.venue.address) georss = ET.SubElement(entry, "georss:featurename") georss.text = ", ".join(location_parts) return entry
[docs] def generate_feed( self, events: Sequence[Event], feed_id: str = "tag:python.org,2025:events", base_url: str = "https://www.python.org", feed_url: str | None = None, ) -> str: """Generate Atom 1.0 feed XML for events. Args: events: Sequence of Event objects to include in feed feed_id: Unique identifier for the feed base_url: Base URL for event links feed_url: URL of the feed itself (for self-reference) Returns: Atom 1.0 XML string """ feed = ET.Element("feed") feed.set("xmlns", self.NAMESPACE) feed.set("xmlns:georss", "http://www.georss.org/georss") title = ET.SubElement(feed, "title") title.text = self.title subtitle = ET.SubElement(feed, "subtitle") subtitle.text = self.subtitle feed_id_elem = ET.SubElement(feed, "id") feed_id_elem.text = feed_id updated = ET.SubElement(feed, "updated") updated.text = _format_rfc3339(datetime.datetime.now(datetime.UTC)) link_alt = ET.SubElement(feed, "link") link_alt.set("href", f"{base_url}/events/") link_alt.set("rel", "alternate") link_alt.set("type", "text/html") if feed_url: link_self = ET.SubElement(feed, "link") link_self.set("href", feed_url) link_self.set("rel", "self") link_self.set("type", "application/atom+xml") author = ET.SubElement(feed, "author") author_name = ET.SubElement(author, "name") author_name.text = self.author_name author_email = ET.SubElement(author, "email") author_email.text = self.author_email generator = ET.SubElement(feed, "generator") generator.set("uri", "https://github.com/litestar-org/litestar") generator.text = "Python.org Litestar" for event in events: feed.append(self._create_entry(event, base_url)) return '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(feed, encoding="unicode")