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
This commit is contained in:
wltb 2014-05-01 01:18:30 +02:00
parent 1e9cf23f65
commit bf6b8af249
5 changed files with 78 additions and 79 deletions

View file

@ -57,13 +57,9 @@ class FeedEntry(object):
self.__extensions = {} self.__extensions = {}
def atom_entry(self, feed, extensions=True): def atom_entry(self, extensions=True):
'''Insert an ATOM entry into a existing XML structure. Normally you '''Create an ATOM entry and return it.'''
would pass the feed node of an ATOM feed XML to this function. entry = etree.Element('entry')
:param feed: The XML element to use as parent node for the element.
'''
entry = etree.SubElement(feed, 'entry')
if not ( self.__atom_id and self.__atom_title and self.__atom_updated ): if not ( self.__atom_id and self.__atom_title and self.__atom_updated ):
raise ValueError('Required fields not set') raise ValueError('Required fields not set')
id = etree.SubElement(entry, 'id') id = etree.SubElement(entry, 'id')
@ -175,13 +171,9 @@ class FeedEntry(object):
return entry return entry
def rss_entry(self, feed, extensions=True): def rss_entry(self, extensions=True):
'''Insert an RSS item into a existing XML structure. Normally you '''Create a RSS item and return it.'''
would pass the channel node of an RSS feed XML to this function. entry = etree.Element('item')
:param feed: The XML element to use as parent node for the item.
'''
entry = etree.SubElement(feed, 'item')
if not ( self.__rss_title or self.__rss_description or self.__rss_content): if not ( self.__rss_title or self.__rss_description or self.__rss_content):
raise ValueError('Required fields not set') raise ValueError('Required fields not set')
if self.__rss_title: if self.__rss_title:
@ -235,7 +227,7 @@ class FeedEntry(object):
return entry return entry
def title(self, title=None): def title(self, title=None):
'''Get or set the title value of the entry. It should contain a human '''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 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. - *name* conveys a human-readable name for the person.
- *uri* contains a home page for the person. - *uri* contains a home page for the person.
- *email* contains an email address for the person. - *email* contains an email address for the person.
:param author: Dict or list of dicts with author data. :param author: Dict or list of dicts with author data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
@ -335,7 +327,7 @@ class FeedEntry(object):
if not author is None: if not author is None:
if replace or self.__atom_author is None: if replace or self.__atom_author is None:
self.__atom_author = [] self.__atom_author = []
self.__atom_author += ensure_format( author, self.__atom_author += ensure_format( author,
set(['name', 'email', 'uri']), set(['name'])) set(['name', 'email', 'uri']), set(['name']))
self.__rss_author = [] self.__rss_author = []
for a in self.__atom_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 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 link element in ATOM feed entries. So for the RSS enclusure element the
last link with rel=enclosure is used. last link with rel=enclosure is used.
:param link: Dict or list of dicts with data. :param link: Dict or list of dicts with data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of link data. :returns: List of link data.
@ -412,9 +404,9 @@ class FeedEntry(object):
if not link is None: if not link is None:
if replace or self.__atom_link is None: if replace or self.__atom_link is None:
self.__atom_link = [] self.__atom_link = []
self.__atom_link += ensure_format( link, self.__atom_link += ensure_format( link,
set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), set(['href', 'rel', 'type', 'hreflang', 'title', 'length']),
set(['href']), set(['href']),
{'rel':['alternate', 'enclosure', 'related', 'self', 'via']}, {'rel':['alternate', 'enclosure', 'related', 'self', 'via']},
{'rel': 'alternate'} ) {'rel': 'alternate'} )
# RSS only needs one URL. We use the first link for RSS: # 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 not category is None:
if replace or self.__atom_category is None: if replace or self.__atom_category is None:
self.__atom_category = [] self.__atom_category = []
self.__atom_category += ensure_format( self.__atom_category += ensure_format(
category, category,
set(['term', 'scheme', 'label']), set(['term', 'scheme', 'label']),
set(['term']) ) set(['term']) )
# Map the ATOM categories to RSS categories. Use the atom:label as # 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. - *name* conveys a human-readable name for the person.
- *uri* contains a home page for the person. - *uri* contains a home page for the person.
- *email* contains an email address for the person. - *email* contains an email address for the person.
:param contributor: Dictionary or list of dictionaries with contributor data. :param contributor: Dictionary or list of dictionaries with contributor data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of contributors as dictionaries. :returns: List of contributors as dictionaries.
@ -535,7 +527,7 @@ class FeedEntry(object):
if not contributor is None: if not contributor is None:
if replace or self.__atom_contributor is None: if replace or self.__atom_contributor is None:
self.__atom_contributor = [] self.__atom_contributor = []
self.__atom_contributor += ensure_format( contributor, self.__atom_contributor += ensure_format( contributor,
set(['name', 'email', 'uri']), set(['name'])) set(['name', 'email', 'uri']), set(['name']))
return self.__atom_contributor return self.__atom_contributor
@ -563,7 +555,7 @@ class FeedEntry(object):
return self.__atom_published return self.__atom_published
def pubdate(self, pubDate=None): def pubdate(self, pubDate=None):
'''Get or set the pubDate of the entry which indicates when the entry was '''Get or set the pubDate of the entry which indicates when the entry was
published. This method is just another name for the published(...) published. This method is just another name for the published(...)
@ -627,7 +619,7 @@ class FeedEntry(object):
self.__rss_ttl = int(ttl) self.__rss_ttl = int(ttl)
return self.__rss_ttl return self.__rss_ttl
def load_extension(self, name, atom=True, rss=True): def load_extension(self, name, atom=True, rss=True):
'''Load a specific extension by name. '''Load a specific extension by name.

View file

@ -15,9 +15,12 @@
class BaseExtension(object): class BaseExtension(object):
'''Basic FeedGenerator extension. '''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): 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. :param feed: The feed xml root element.
:returns: The feed root element. :returns: The feed root element.
@ -26,7 +29,7 @@ class BaseExtension(object):
def extend_atom(self, feed): 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. fields.
:param feed: The feed xml root element. :param feed: The feed xml root element.

View file

@ -42,6 +42,8 @@ class DcBaseExtension(BaseExtension):
self._dcelem_title = None self._dcelem_title = None
self._dcelem_type = None self._dcelem_type = None
def extend_ns(self):
return {'dc' : 'http://purl.org/dc/elements/1.1/'}
def extend_atom(self, atom_feed): def extend_atom(self, atom_feed):
'''Create an Atom feed xml structure containing all previously set fields. '''Create an Atom feed xml structure containing all previously set fields.
@ -49,14 +51,8 @@ class DcBaseExtension(BaseExtension):
:returns: The feed root element :returns: The feed root element
''' '''
DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/' DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/'
# Replace the root element to add the new namespace
nsmap = atom_feed.nsmap feed = atom_feed
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[:]
for elem in ['contributor', 'coverage', 'creator', 'date', 'description', for elem in ['contributor', 'coverage', 'creator', 'date', 'description',
'language', 'publisher', 'relation', 'rights', 'source', 'subject', 'language', 'publisher', 'relation', 'rights', 'source', 'subject',
@ -83,12 +79,7 @@ class DcBaseExtension(BaseExtension):
:returns: Tuple containing the feed root element and the element tree. :returns: Tuple containing the feed root element and the element tree.
''' '''
DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/' DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/'
# Replace the root element to add the new namespace channel = rss_feed[0]
nsmap = rss_feed.nsmap
nsmap['dc'] = DCELEMENTS_NS
feed = etree.Element('rss', version='2.0', nsmap=nsmap )
feed[:] = rss_feed[:]
channel = feed[0]
for elem in ['contributor', 'coverage', 'creator', 'date', 'description', for elem in ['contributor', 'coverage', 'creator', 'date', 'description',
'language', 'publisher', 'relation', 'rights', 'source', 'subject', 'language', 'publisher', 'relation', 'rights', 'source', 'subject',
@ -106,7 +97,7 @@ class DcBaseExtension(BaseExtension):
node = etree.SubElement(channel, '{%s}identifier' % DCELEMENTS_NS) node = etree.SubElement(channel, '{%s}identifier' % DCELEMENTS_NS)
node.text = identifier node.text = identifier
return feed return rss_feed
def dc_contributor(self, contributor=None, replace=False): def dc_contributor(self, contributor=None, replace=False):

View file

@ -34,6 +34,8 @@ class PodcastExtension(BaseExtension):
self.__itunes_summary = None 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): def extend_rss(self, rss_feed):
@ -42,11 +44,7 @@ class PodcastExtension(BaseExtension):
:returns: Tuple containing the feed root element and the element tree. :returns: Tuple containing the feed root element and the element tree.
''' '''
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
# Replace the root element to add the itunes namespace feed = rss_feed
nsmap = rss_feed.nsmap
nsmap['itunes'] = ITUNES_NS
feed = etree.Element('rss', version='2.0', nsmap=nsmap )
feed[:] = rss_feed[:]
channel = feed[0] channel = feed[0]
if self.__itunes_author: if self.__itunes_author:

View file

@ -88,7 +88,13 @@ class FeedGenerator(object):
:returns: Tuple containing the feed root element and the element tree. :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: if self.__atom_feed_xml_lang:
feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = \ feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = \
self.__atom_feed_xml_lang self.__atom_feed_xml_lang
@ -182,10 +188,11 @@ class FeedGenerator(object):
if extensions: if extensions:
for ext in self.__extensions.values() or []: for ext in self.__extensions.values() or []:
if ext.get('atom'): if ext.get('atom'):
feed = ext['inst'].extend_atom(feed) ext['inst'].extend_atom(feed)
for entry in self.__feed_entries: for entry in self.__feed_entries:
entry.atom_entry(feed) entry = entry.atom_entry()
feed.append(entry)
doc = etree.ElementTree(feed) doc = etree.ElementTree(feed)
return feed, doc return feed, doc
@ -193,7 +200,7 @@ class FeedGenerator(object):
def atom_str(self, pretty=False, extensions=True): def atom_str(self, pretty=False, extensions=True):
'''Generates an ATOM feed and returns the feed XML as string. '''Generates an ATOM feed and returns the feed XML as string.
:param pretty: If the feed should be split into multiple lines and :param pretty: If the feed should be split into multiple lines and
properly indented. properly indented.
:param extensions: Enable or disable the loaded extensions for the xml :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): def atom_file(self, filename, extensions=True, pretty=False):
'''Generates an ATOM feed and write the resulting XML to a file. '''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 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 :param extensions: Enable or disable the loaded extensions for the xml
generation (default: enabled). generation (default: enabled).
@ -220,9 +227,16 @@ class FeedGenerator(object):
:returns: Tuple containing the feed root element and the element tree. :returns: Tuple containing the feed root element and the element tree.
''' '''
feed = etree.Element('rss', version='2.0', nsmap = dict()
nsmap={'atom': 'http://www.w3.org/2005/Atom', if extensions:
'content': 'http://purl.org/rss/1.0/modules/content/'} ) 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') channel = etree.SubElement(feed, 'channel')
if not ( self.__rss_title and self.__rss_link and self.__rss_description ): if not ( self.__rss_title and self.__rss_link and self.__rss_description ):
missing = ', '.join(([] if self.__rss_title else ['title']) + \ missing = ', '.join(([] if self.__rss_title else ['title']) + \
@ -238,8 +252,8 @@ class FeedGenerator(object):
for ln in self.__atom_link or []: for ln in self.__atom_link or []:
# It is recommended to include a atom self link in rss documents… # It is recommended to include a atom self link in rss documents…
if ln.get('rel') == 'self': if ln.get('rel') == 'self':
selflink = etree.SubElement(channel, selflink = etree.SubElement(channel,
'{http://www.w3.org/2005/Atom}link', '{http://www.w3.org/2005/Atom}link',
href=ln['href'], rel='self') href=ln['href'], rel='self')
if ln.get('type'): if ln.get('type'):
selflink.attrib['type'] = ln['type'] selflink.attrib['type'] = ln['type']
@ -335,10 +349,11 @@ class FeedGenerator(object):
if extensions: if extensions:
for ext in self.__extensions.values() or []: for ext in self.__extensions.values() or []:
if ext.get('rss'): if ext.get('rss'):
feed = ext['inst'].extend_rss(feed) ext['inst'].extend_rss(feed)
for entry in self.__feed_entries: for entry in self.__feed_entries:
entry.rss_entry(channel) item = entry.rss_entry()
channel.append(item)
doc = etree.ElementTree(feed) doc = etree.ElementTree(feed)
return feed, doc return feed, doc
@ -346,7 +361,7 @@ class FeedGenerator(object):
def rss_str(self, pretty=False, extensions=True): def rss_str(self, pretty=False, extensions=True):
'''Generates an RSS feed and returns the feed XML as string. '''Generates an RSS feed and returns the feed XML as string.
:param pretty: If the feed should be split into multiple lines and :param pretty: If the feed should be split into multiple lines and
properly indented. properly indented.
:param extensions: Enable or disable the loaded extensions for the xml :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): def rss_file(self, filename, extensions=True, pretty=False):
'''Generates an RSS feed and write the resulting XML to a file. '''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 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 :param extensions: Enable or disable the loaded extensions for the xml
generation (default: enabled). generation (default: enabled).
@ -367,7 +382,7 @@ class FeedGenerator(object):
feed, doc = self._create_rss(extensions=extensions) feed, doc = self._create_rss(extensions=extensions)
doc.write(filename, pretty_print=pretty) doc.write(filename, pretty_print=pretty)
def title(self, title=None): def title(self, title=None):
'''Get or set the title value of the feed. It should contain a human '''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 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. :param id: New Id of the ATOM feed.
:returns: Id of the feed. :returns: Id of the feed.
''' '''
if not id is None: if not id is None:
self.__atom_id = id self.__atom_id = id
return self.__atom_id return self.__atom_id
@ -462,7 +477,7 @@ class FeedGenerator(object):
- *name* conveys a human-readable name for the person. - *name* conveys a human-readable name for the person.
- *uri* contains a home page for the person. - *uri* contains a home page for the person.
- *email* contains an email address for the person. - *email* contains an email address for the person.
:param author: Dictionary or list of dictionaries with author data. :param author: Dictionary or list of dictionaries with author data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of authors as dictionaries. :returns: List of authors as dictionaries.
@ -485,7 +500,7 @@ class FeedGenerator(object):
if not author is None: if not author is None:
if replace or self.__atom_author is None: if replace or self.__atom_author is None:
self.__atom_author = [] self.__atom_author = []
self.__atom_author += ensure_format( author, self.__atom_author += ensure_format( author,
set(['name', 'email', 'uri']), set(['name'])) set(['name', 'email', 'uri']), set(['name']))
self.__rss_author = [] self.__rss_author = []
for a in self.__atom_author: for a in self.__atom_author:
@ -526,7 +541,7 @@ class FeedGenerator(object):
- *length* the length of the resource, in bytes. - *length* the length of the resource, in bytes.
RSS only supports one link with URL only. RSS only supports one link with URL only.
:param link: Dict or list of dicts with data. :param link: Dict or list of dicts with data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
@ -541,9 +556,9 @@ class FeedGenerator(object):
if not link is None: if not link is None:
if replace or self.__atom_link is None: if replace or self.__atom_link is None:
self.__atom_link = [] self.__atom_link = []
self.__atom_link += ensure_format( link, self.__atom_link += ensure_format( link,
set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), set(['href', 'rel', 'type', 'hreflang', 'title', 'length']),
set(['href']), set(['href']),
{'rel':['alternate', 'enclosure', 'related', 'self', 'via']} ) {'rel':['alternate', 'enclosure', 'related', 'self', 'via']} )
# RSS only needs one URL. We use the first link for RSS: # RSS only needs one URL. We use the first link for RSS:
if len(self.__atom_link) > 0: if len(self.__atom_link) > 0:
@ -579,8 +594,8 @@ class FeedGenerator(object):
if not category is None: if not category is None:
if replace or self.__atom_category is None: if replace or self.__atom_category is None:
self.__atom_category = [] self.__atom_category = []
self.__atom_category += ensure_format( self.__atom_category += ensure_format(
category, category,
set(['term', 'scheme', 'label']), set(['term', 'scheme', 'label']),
set(['term']) ) set(['term']) )
# Map the ATOM categories to RSS categories. Use the atom:label as # 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. - *name* conveys a human-readable name for the person.
- *uri* contains a home page for the person. - *uri* contains a home page for the person.
- *email* contains an email address for the person. - *email* contains an email address for the person.
:param contributor: Dictionary or list of dictionaries with contributor data. :param contributor: Dictionary or list of dictionaries with contributor data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of contributors as dictionaries. :returns: List of contributors as dictionaries.
@ -638,7 +653,7 @@ class FeedGenerator(object):
if not contributor is None: if not contributor is None:
if replace or self.__atom_contributor is None: if replace or self.__atom_contributor is None:
self.__atom_contributor = [] self.__atom_contributor = []
self.__atom_contributor += ensure_format( contributor, self.__atom_contributor += ensure_format( contributor,
set(['name', 'email', 'uri']), set(['name'])) set(['name', 'email', 'uri']), set(['name']))
return self.__atom_contributor 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 This method can be called with an hour or a list of hours. The hours are
represented as integer values from 0 to 23. represented as integer values from 0 to 23.
:param hours: List of hours the feedreaders should not check the feed. :param hours: List of hours the feedreaders should not check the feed.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of hours the feedreaders should not check the feed. :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 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'. represented as strings from 'Monday' to 'Sunday'.
:param hours: List of days the feedreaders should not check the feed. :param hours: List of days the feedreaders should not check the feed.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of days the feedreaders should not check the feed. :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. :returns: FeedEntry object created or passed to this function.
Example:: Example::
... ...
>>> entry = feedgen.add_entry() >>> entry = feedgen.add_entry()
>>> entry.title('First feed entry') >>> entry.title('First feed entry')
@ -984,7 +999,7 @@ class FeedGenerator(object):
def entry(self, entry=None, replace=False): def entry(self, entry=None, replace=False):
'''Get or set feed entries. Use the add_entry() method instead to '''Get or set feed entries. Use the add_entry() method instead to
automatically create the FeedEntry objects. automatically create the FeedEntry objects.
This method takes both a single FeedEntry object or a list of objects. This method takes both a single FeedEntry object or a list of objects.
:param entry: FeedEntry object or list of FeedEntry objects. :param entry: FeedEntry object or list of FeedEntry objects.
@ -1025,7 +1040,7 @@ class FeedGenerator(object):
self.__feed_entries.remove(entry) self.__feed_entries.remove(entry)
else: else:
self.__feed_entries.pop(entry) self.__feed_entries.pop(entry)
def remove_item(self, item): def remove_item(self, item):
'''Remove a single item from the feed. This is another name for '''Remove a single item from the feed. This is another name for
@ -1033,7 +1048,7 @@ class FeedGenerator(object):
''' '''
self.remove_entry(item) self.remove_entry(item)
def load_extension(self, name, atom=True, rss=True): def load_extension(self, name, atom=True, rss=True):
'''Load a specific extension by name. '''Load a specific extension by name.