From bf6b8af249d0130e914825179a3bcd17666aaf6a Mon Sep 17 00:00:00 2001 From: wltb Date: Thu, 1 May 2014 01:18:30 +0200 Subject: [PATCH] Feed Creation: Add a separate extension call for namespace stuff. Change extensions accordingly. Entry creation: Entries create their own XML Element, which is put into the feed by the caller --- feedgen/entry.py | 44 ++++++++++-------------- feedgen/ext/base.py | 7 ++-- feedgen/ext/dc.py | 21 ++++-------- feedgen/ext/podcast.py | 8 ++--- feedgen/feed.py | 77 +++++++++++++++++++++++++----------------- 5 files changed, 78 insertions(+), 79 deletions(-) diff --git a/feedgen/entry.py b/feedgen/entry.py index 884d103..499c387 100644 --- a/feedgen/entry.py +++ b/feedgen/entry.py @@ -57,13 +57,9 @@ class FeedEntry(object): self.__extensions = {} - def atom_entry(self, feed, extensions=True): - '''Insert an ATOM entry into a existing XML structure. Normally you - would pass the feed node of an ATOM feed XML to this function. - - :param feed: The XML element to use as parent node for the element. - ''' - entry = etree.SubElement(feed, 'entry') + def atom_entry(self, extensions=True): + '''Create an ATOM entry and return it.''' + entry = etree.Element('entry') if not ( self.__atom_id and self.__atom_title and self.__atom_updated ): raise ValueError('Required fields not set') id = etree.SubElement(entry, 'id') @@ -175,13 +171,9 @@ class FeedEntry(object): return entry - def rss_entry(self, feed, extensions=True): - '''Insert an RSS item into a existing XML structure. Normally you - would pass the channel node of an RSS feed XML to this function. - - :param feed: The XML element to use as parent node for the item. - ''' - entry = etree.SubElement(feed, 'item') + def rss_entry(self, extensions=True): + '''Create a RSS item and return it.''' + entry = etree.Element('item') if not ( self.__rss_title or self.__rss_description or self.__rss_content): raise ValueError('Required fields not set') if self.__rss_title: @@ -235,7 +227,7 @@ class FeedEntry(object): return entry - + def title(self, title=None): '''Get or set the title value of the entry. It should contain a human readable title for the entry. Title is mandatory for both ATOM and RSS @@ -313,7 +305,7 @@ class FeedEntry(object): - *name* conveys a human-readable name for the person. - *uri* contains a home page for the person. - *email* contains an email address for the person. - + :param author: Dict or list of dicts with author data. :param replace: Add or replace old data. @@ -335,7 +327,7 @@ class FeedEntry(object): if not author is None: if replace or self.__atom_author is None: self.__atom_author = [] - self.__atom_author += ensure_format( author, + self.__atom_author += ensure_format( author, set(['name', 'email', 'uri']), set(['name'])) self.__rss_author = [] for a in self.__atom_author: @@ -402,7 +394,7 @@ class FeedEntry(object): RSS also supports one enclusure element per entry which is covered by the link element in ATOM feed entries. So for the RSS enclusure element the last link with rel=enclosure is used. - + :param link: Dict or list of dicts with data. :param replace: Add or replace old data. :returns: List of link data. @@ -412,9 +404,9 @@ class FeedEntry(object): if not link is None: if replace or self.__atom_link is None: self.__atom_link = [] - self.__atom_link += ensure_format( link, + self.__atom_link += ensure_format( link, set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), - set(['href']), + set(['href']), {'rel':['alternate', 'enclosure', 'related', 'self', 'via']}, {'rel': 'alternate'} ) # RSS only needs one URL. We use the first link for RSS: @@ -495,8 +487,8 @@ class FeedEntry(object): if not category is None: if replace or self.__atom_category is None: self.__atom_category = [] - self.__atom_category += ensure_format( - category, + self.__atom_category += ensure_format( + category, set(['term', 'scheme', 'label']), set(['term']) ) # Map the ATOM categories to RSS categories. Use the atom:label as @@ -525,7 +517,7 @@ class FeedEntry(object): - *name* conveys a human-readable name for the person. - *uri* contains a home page for the person. - *email* contains an email address for the person. - + :param contributor: Dictionary or list of dictionaries with contributor data. :param replace: Add or replace old data. :returns: List of contributors as dictionaries. @@ -535,7 +527,7 @@ class FeedEntry(object): if not contributor is None: if replace or self.__atom_contributor is None: self.__atom_contributor = [] - self.__atom_contributor += ensure_format( contributor, + self.__atom_contributor += ensure_format( contributor, set(['name', 'email', 'uri']), set(['name'])) return self.__atom_contributor @@ -563,7 +555,7 @@ class FeedEntry(object): return self.__atom_published - + def pubdate(self, pubDate=None): '''Get or set the pubDate of the entry which indicates when the entry was published. This method is just another name for the published(...) @@ -627,7 +619,7 @@ class FeedEntry(object): self.__rss_ttl = int(ttl) return self.__rss_ttl - + def load_extension(self, name, atom=True, rss=True): '''Load a specific extension by name. diff --git a/feedgen/ext/base.py b/feedgen/ext/base.py index 5042264..da7f571 100644 --- a/feedgen/ext/base.py +++ b/feedgen/ext/base.py @@ -15,9 +15,12 @@ class BaseExtension(object): '''Basic FeedGenerator extension. ''' + def extend_ns(self): + '''Returns a dict that will be used in the namespace map for the feed.''' + return dict() def extend_rss(self, feed): - '''Create an RSS feed xml structure containing all previously set fields. + '''Extend a RSS feed xml structure containing all previously set fields. :param feed: The feed xml root element. :returns: The feed root element. @@ -26,7 +29,7 @@ class BaseExtension(object): def extend_atom(self, feed): - '''Create an ATOM feed xml structure containing all previously set + '''Extend an ATOM feed xml structure containing all previously set fields. :param feed: The feed xml root element. diff --git a/feedgen/ext/dc.py b/feedgen/ext/dc.py index cc87fa7..d5c95e9 100644 --- a/feedgen/ext/dc.py +++ b/feedgen/ext/dc.py @@ -42,6 +42,8 @@ class DcBaseExtension(BaseExtension): self._dcelem_title = None self._dcelem_type = None + def extend_ns(self): + return {'dc' : 'http://purl.org/dc/elements/1.1/'} def extend_atom(self, atom_feed): '''Create an Atom feed xml structure containing all previously set fields. @@ -49,14 +51,8 @@ class DcBaseExtension(BaseExtension): :returns: The feed root element ''' DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/' - # Replace the root element to add the new namespace - nsmap = atom_feed.nsmap - nsmap['dc'] = DCELEMENTS_NS - feed = etree.Element('feed', nsmap=nsmap) - if '{http://www.w3.org/XML/1998/namespace}lang' in atom_feed.attrib: - feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = \ - atom_feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] - feed[:] = atom_feed[:] + + feed = atom_feed for elem in ['contributor', 'coverage', 'creator', 'date', 'description', 'language', 'publisher', 'relation', 'rights', 'source', 'subject', @@ -83,12 +79,7 @@ class DcBaseExtension(BaseExtension): :returns: Tuple containing the feed root element and the element tree. ''' DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/' - # Replace the root element to add the new namespace - nsmap = rss_feed.nsmap - nsmap['dc'] = DCELEMENTS_NS - feed = etree.Element('rss', version='2.0', nsmap=nsmap ) - feed[:] = rss_feed[:] - channel = feed[0] + channel = rss_feed[0] for elem in ['contributor', 'coverage', 'creator', 'date', 'description', 'language', 'publisher', 'relation', 'rights', 'source', 'subject', @@ -106,7 +97,7 @@ class DcBaseExtension(BaseExtension): node = etree.SubElement(channel, '{%s}identifier' % DCELEMENTS_NS) node.text = identifier - return feed + return rss_feed def dc_contributor(self, contributor=None, replace=False): diff --git a/feedgen/ext/podcast.py b/feedgen/ext/podcast.py index 558ae4e..26494fe 100644 --- a/feedgen/ext/podcast.py +++ b/feedgen/ext/podcast.py @@ -34,6 +34,8 @@ class PodcastExtension(BaseExtension): self.__itunes_summary = None + def extend_ns(self): + return {'itunes' : 'http://www.itunes.com/dtds/podcast-1.0.dtd'} def extend_rss(self, rss_feed): @@ -42,11 +44,7 @@ class PodcastExtension(BaseExtension): :returns: Tuple containing the feed root element and the element tree. ''' ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' - # Replace the root element to add the itunes namespace - nsmap = rss_feed.nsmap - nsmap['itunes'] = ITUNES_NS - feed = etree.Element('rss', version='2.0', nsmap=nsmap ) - feed[:] = rss_feed[:] + feed = rss_feed channel = feed[0] if self.__itunes_author: diff --git a/feedgen/feed.py b/feedgen/feed.py index 4cc1d8d..b4bebb5 100644 --- a/feedgen/feed.py +++ b/feedgen/feed.py @@ -88,7 +88,13 @@ class FeedGenerator(object): :returns: Tuple containing the feed root element and the element tree. ''' - feed = etree.Element('feed', xmlns='http://www.w3.org/2005/Atom') + nsmap = dict() + if extensions: + for ext in self.__extensions.values() or []: + if ext.get('atom'): + nsmap.update( ext['inst'].extend_ns() ) + + feed = etree.Element('feed', xmlns='http://www.w3.org/2005/Atom', nsmap=nsmap) if self.__atom_feed_xml_lang: feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = \ self.__atom_feed_xml_lang @@ -182,10 +188,11 @@ class FeedGenerator(object): if extensions: for ext in self.__extensions.values() or []: if ext.get('atom'): - feed = ext['inst'].extend_atom(feed) + ext['inst'].extend_atom(feed) for entry in self.__feed_entries: - entry.atom_entry(feed) + entry = entry.atom_entry() + feed.append(entry) doc = etree.ElementTree(feed) return feed, doc @@ -193,7 +200,7 @@ class FeedGenerator(object): def atom_str(self, pretty=False, extensions=True): '''Generates an ATOM feed and returns the feed XML as string. - + :param pretty: If the feed should be split into multiple lines and properly indented. :param extensions: Enable or disable the loaded extensions for the xml @@ -206,7 +213,7 @@ class FeedGenerator(object): def atom_file(self, filename, extensions=True, pretty=False): '''Generates an ATOM feed and write the resulting XML to a file. - + :param filename: Name of file to write, or a file-like object, or a URL. :param extensions: Enable or disable the loaded extensions for the xml generation (default: enabled). @@ -220,9 +227,16 @@ class FeedGenerator(object): :returns: Tuple containing the feed root element and the element tree. ''' - feed = etree.Element('rss', version='2.0', - nsmap={'atom': 'http://www.w3.org/2005/Atom', - 'content': 'http://purl.org/rss/1.0/modules/content/'} ) + nsmap = dict() + if extensions: + for ext in self.__extensions.values() or []: + if ext.get('rss'): + nsmap.update( ext['inst'].extend_ns() ) + + nsmap.update({'atom': 'http://www.w3.org/2005/Atom', + 'content': 'http://purl.org/rss/1.0/modules/content/'}) + + feed = etree.Element('rss', version='2.0', nsmap=nsmap ) channel = etree.SubElement(feed, 'channel') if not ( self.__rss_title and self.__rss_link and self.__rss_description ): missing = ', '.join(([] if self.__rss_title else ['title']) + \ @@ -238,8 +252,8 @@ class FeedGenerator(object): for ln in self.__atom_link or []: # It is recommended to include a atom self link in rss documents… if ln.get('rel') == 'self': - selflink = etree.SubElement(channel, - '{http://www.w3.org/2005/Atom}link', + selflink = etree.SubElement(channel, + '{http://www.w3.org/2005/Atom}link', href=ln['href'], rel='self') if ln.get('type'): selflink.attrib['type'] = ln['type'] @@ -335,10 +349,11 @@ class FeedGenerator(object): if extensions: for ext in self.__extensions.values() or []: if ext.get('rss'): - feed = ext['inst'].extend_rss(feed) + ext['inst'].extend_rss(feed) for entry in self.__feed_entries: - entry.rss_entry(channel) + item = entry.rss_entry() + channel.append(item) doc = etree.ElementTree(feed) return feed, doc @@ -346,7 +361,7 @@ class FeedGenerator(object): def rss_str(self, pretty=False, extensions=True): '''Generates an RSS feed and returns the feed XML as string. - + :param pretty: If the feed should be split into multiple lines and properly indented. :param extensions: Enable or disable the loaded extensions for the xml @@ -359,7 +374,7 @@ class FeedGenerator(object): def rss_file(self, filename, extensions=True, pretty=False): '''Generates an RSS feed and write the resulting XML to a file. - + :param filename: Name of file to write, or a file-like object, or a URL. :param extensions: Enable or disable the loaded extensions for the xml generation (default: enabled). @@ -367,7 +382,7 @@ class FeedGenerator(object): feed, doc = self._create_rss(extensions=extensions) doc.write(filename, pretty_print=pretty) - + def title(self, title=None): '''Get or set the title value of the feed. It should contain a human readable title for the feed. Often the same as the title of the @@ -392,7 +407,7 @@ class FeedGenerator(object): :param id: New Id of the ATOM feed. :returns: Id of the feed. ''' - + if not id is None: self.__atom_id = id return self.__atom_id @@ -462,7 +477,7 @@ class FeedGenerator(object): - *name* conveys a human-readable name for the person. - *uri* contains a home page for the person. - *email* contains an email address for the person. - + :param author: Dictionary or list of dictionaries with author data. :param replace: Add or replace old data. :returns: List of authors as dictionaries. @@ -485,7 +500,7 @@ class FeedGenerator(object): if not author is None: if replace or self.__atom_author is None: self.__atom_author = [] - self.__atom_author += ensure_format( author, + self.__atom_author += ensure_format( author, set(['name', 'email', 'uri']), set(['name'])) self.__rss_author = [] for a in self.__atom_author: @@ -526,7 +541,7 @@ class FeedGenerator(object): - *length* the length of the resource, in bytes. RSS only supports one link with URL only. - + :param link: Dict or list of dicts with data. :param replace: Add or replace old data. @@ -541,9 +556,9 @@ class FeedGenerator(object): if not link is None: if replace or self.__atom_link is None: self.__atom_link = [] - self.__atom_link += ensure_format( link, + self.__atom_link += ensure_format( link, set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), - set(['href']), + set(['href']), {'rel':['alternate', 'enclosure', 'related', 'self', 'via']} ) # RSS only needs one URL. We use the first link for RSS: if len(self.__atom_link) > 0: @@ -579,8 +594,8 @@ class FeedGenerator(object): if not category is None: if replace or self.__atom_category is None: self.__atom_category = [] - self.__atom_category += ensure_format( - category, + self.__atom_category += ensure_format( + category, set(['term', 'scheme', 'label']), set(['term']) ) # Map the ATOM categories to RSS categories. Use the atom:label as @@ -628,7 +643,7 @@ class FeedGenerator(object): - *name* conveys a human-readable name for the person. - *uri* contains a home page for the person. - *email* contains an email address for the person. - + :param contributor: Dictionary or list of dictionaries with contributor data. :param replace: Add or replace old data. :returns: List of contributors as dictionaries. @@ -638,7 +653,7 @@ class FeedGenerator(object): if not contributor is None: if replace or self.__atom_contributor is None: self.__atom_contributor = [] - self.__atom_contributor += ensure_format( contributor, + self.__atom_contributor += ensure_format( contributor, set(['name', 'email', 'uri']), set(['name'])) return self.__atom_contributor @@ -856,7 +871,7 @@ class FeedGenerator(object): This method can be called with an hour or a list of hours. The hours are represented as integer values from 0 to 23. - + :param hours: List of hours the feedreaders should not check the feed. :param replace: Add or replace old data. :returns: List of hours the feedreaders should not check the feed. @@ -879,7 +894,7 @@ class FeedGenerator(object): This method can be called with a day name or a list of day names. The days are represented as strings from 'Monday' to 'Sunday'. - + :param hours: List of days the feedreaders should not check the feed. :param replace: Add or replace old data. :returns: List of days the feedreaders should not check the feed. @@ -953,7 +968,7 @@ class FeedGenerator(object): :returns: FeedEntry object created or passed to this function. Example:: - + ... >>> entry = feedgen.add_entry() >>> entry.title('First feed entry') @@ -984,7 +999,7 @@ class FeedGenerator(object): def entry(self, entry=None, replace=False): '''Get or set feed entries. Use the add_entry() method instead to automatically create the FeedEntry objects. - + This method takes both a single FeedEntry object or a list of objects. :param entry: FeedEntry object or list of FeedEntry objects. @@ -1025,7 +1040,7 @@ class FeedGenerator(object): self.__feed_entries.remove(entry) else: self.__feed_entries.pop(entry) - + def remove_item(self, item): '''Remove a single item from the feed. This is another name for @@ -1033,7 +1048,7 @@ class FeedGenerator(object): ''' self.remove_entry(item) - + def load_extension(self, name, atom=True, rss=True): '''Load a specific extension by name.