Added extension support and converted PodcastGenerator to extension

This commit is contained in:
Lars Kiesow 2013-05-05 18:17:44 +02:00
parent e9eb3b5e95
commit 840dc7e5b8
8 changed files with 200 additions and 78 deletions

View file

@ -9,7 +9,6 @@
''' '''
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from feedgen.podcast import PodcastGenerator
import sys import sys
@ -32,7 +31,7 @@ if __name__ == '__main__':
arg = sys.argv[1] arg = sys.argv[1]
fg = PodcastGenerator() if arg.endswith('podcast') else FeedGenerator() fg = FeedGenerator()
fg.id('http://lernfunk.de/_MEDIAID_123') fg.id('http://lernfunk.de/_MEDIAID_123')
fg.title('Testfeed') fg.title('Testfeed')
fg.author( {'name':'Lars Kiesow','email':'lkiesow@uos.de'} ) fg.author( {'name':'Lars Kiesow','email':'lkiesow@uos.de'} )
@ -64,16 +63,21 @@ if __name__ == '__main__':
elif arg == 'rss': elif arg == 'rss':
print fg.rss_str(pretty=True) print fg.rss_str(pretty=True)
elif arg == 'podcast': elif arg == 'podcast':
fg.itunes_author('Lars Kiesow') # Load the podcast extension. It will automatically be loaded for all
fg.itunes_category('Technology', 'Podcasting') # entries in the feed, too. Thus also for our “fe”.
fg.itunes_explicit('no') fg.load_extension('podcast')
fg.itunes_complete('no') fg.podcast.itunes_author('Lars Kiesow')
fg.itunes_new_feed_url('http://example.com/new-feed.rss') fg.podcast.itunes_category('Technology', 'Podcasting')
fg.itunes_owner('John Doe', 'john@example.com') fg.podcast.itunes_explicit('no')
fg.itunes_summary('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Verba tu fingas et ea dicas, quae non sentias?') fg.podcast.itunes_complete('no')
fe.itunes_author('Lars Kiesow') fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss')
fg.podcast.itunes_owner('John Doe', 'john@example.com')
fg.podcast.itunes_summary('Lorem ipsum dolor sit amet, ' + \
'consectetur adipiscing elit. ' + \
'Verba tu fingas et ea dicas, quae non sentias?')
fe.podcast.itunes_author('Lars Kiesow')
print fg.podcast_str(pretty=True) print fg.rss_str(pretty=True)
elif arg.endswith('atom'): elif arg.endswith('atom'):
fg.atom_file(arg) fg.atom_file(arg)
elif arg.endswith('rss'): elif arg.endswith('rss'):

View file

@ -50,8 +50,11 @@ class FeedEntry(object):
__rss_source = None __rss_source = None
__rss_title = None __rss_title = None
# Extension list:
__extensions = {}
def atom_entry(self, feed):
def atom_entry(self, feed, extensions=True):
'''Insert an ATOM entry into a existing XML structure. Normally you '''Insert an ATOM entry into a existing XML structure. Normally you
would pass the feed node of an ATOM feed XML to this function. would pass the feed node of an ATOM feed XML to this function.
@ -139,10 +142,15 @@ class FeedEntry(object):
rights = etree.SubElement(feed, 'rights') rights = etree.SubElement(feed, 'rights')
rights.text = self.__atom_rights rights.text = self.__atom_rights
if extensions:
for ext in self.__extensions.values() or []:
if ext.get('atom'):
entry = ext['inst'].extend_atom(entry)
return entry return entry
def rss_entry(self, feed): def rss_entry(self, feed, extensions=True):
'''Insert an RSS item into a existing XML structure. Normally you '''Insert an RSS item into a existing XML structure. Normally you
would pass the channel node of an RSS feed XML to this function. would pass the channel node of an RSS feed XML to this function.
@ -184,6 +192,12 @@ class FeedEntry(object):
pubDate = etree.SubElement(channel, 'pubDate') pubDate = etree.SubElement(channel, 'pubDate')
pubDate.text = self.__rss_pubDate.strftime( pubDate.text = self.__rss_pubDate.strftime(
'%a, %e %b %Y %H:%M:%S %z') '%a, %e %b %Y %H:%M:%S %z')
if extensions:
for ext in self.__extensions.values() or []:
if ext.get('rss'):
entry = ext['inst'].extend_rss(entry)
return entry return entry
@ -575,3 +589,34 @@ class FeedEntry(object):
if not ttl is None: if not ttl is None:
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):
'''Load a specific extension by name.
:param name: Name of the extension to load.
:param atom: If the extension should be used for ATOM feeds.
:param rss: If the extension should be used for RSS feeds.
'''
# Check loaded extensions
if not isinstance(self.__extensions, dict):
self.__extensions = {}
if name in self.__extensions.keys():
raise ImportError('Extension already loaded')
# Load extension
extname = name[0].upper() + name[1:] + 'EntryExtension'
# Try to import extension from dedicated module for entry:
try:
supmod = __import__('feedgen.ext.%s_entry' % name)
extmod = getattr(supmod.ext, name + '_entry')
except ImportError:
# Try the FeedExtension module instead
supmod = __import__('feedgen.ext.%s' % name)
extmod = getattr(supmod.ext, name)
ext = getattr(extmod, extname)
extinst = ext()
setattr(self, name, extinst)
self.__extensions[name] = {'inst':extinst,'atom':atom,'rss':rss}

6
feedgen/ext/__init__.py Normal file
View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
"""
===========
feedgen.ext
===========
"""

39
feedgen/ext/base.py Normal file
View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
'''
feedgen.ext.base
~~~~~~~~~~~~~~~~
Basic FeedGenerator which does nothing but provides all necessary methods.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details.
'''
class BaseExtension(object):
'''Basic FeedGenerator extension.
'''
def extend_rss(self, feed):
'''Create an RSS feed xml structure containing all previously set fields.
:param feed: The feed xml root element.
:returns: The feed root element.
'''
return feed
def extend_atom(self, feed, doc=None):
'''Create an ATOM feed xml structure containing all previously set
fields.
:param feed: The feed xml root element.
:returns: The feed root element.
'''
return feed, doc
class BaseEntryExtension(BaseExtension):
'''Basic FeedEntry extension.
'''

View file

@ -14,12 +14,11 @@ from lxml import etree
from datetime import datetime from datetime import datetime
import dateutil.parser import dateutil.parser
import dateutil.tz import dateutil.tz
from feedgen.feed import FeedGenerator from feedgen.ext.base import BaseExtension
from feedgen.podcast_entry import PodcastEntry
from feedgen.util import ensure_format from feedgen.util import ensure_format
class PodcastGenerator(FeedGenerator): class PodcastExtension(BaseExtension):
'''FeedGenerator extension for podcasts. '''FeedGenerator extension for podcasts.
''' '''
@ -40,21 +39,18 @@ class PodcastGenerator(FeedGenerator):
def __create_podcast(self): def extend_rss(self, rss_feed):
'''Create an RSS feed xml structure containing all previously set fields. '''Create an RSS feed xml structure containing all previously set fields.
:returns: Tuple containing the feed root element and the element tree. :returns: Tuple containing the feed root element and the element tree.
''' '''
rss_feed, _ = super(PodcastGenerator,self)._create_rss()
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 # Replace the root element to add the itunes namespace
feed = etree.Element('rss', version='2.0', nsmap = rss_feed.nsmap
nsmap={ nsmap['itunes'] = ITUNES_NS
'atom' :'http://www.w3.org/2005/Atom', feed = etree.Element('rss', version='2.0', nsmap=nsmap )
'itunes':ITUNES_NS} )
feed[:] = rss_feed[:] feed[:] = rss_feed[:]
channel = feed[0] channel = feed[0]
doc = etree.ElementTree(feed)
if self.__itunes_author: if self.__itunes_author:
author = etree.SubElement(channel, '{%s}author' % ITUNES_NS) author = etree.SubElement(channel, '{%s}author' % ITUNES_NS)
@ -102,28 +98,7 @@ class PodcastGenerator(FeedGenerator):
summary = etree.SubElement(channel, '{%s}summary' % ITUNES_NS) summary = etree.SubElement(channel, '{%s}summary' % ITUNES_NS)
summary.text = self.__itunes_summary summary.text = self.__itunes_summary
return feed, doc return feed
def podcast_str(self, pretty=False):
'''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.
:returns: String representation of the RSS feed.
'''
feed, doc = self.__create_podcast()
return etree.tostring(feed, pretty_print=pretty)
def podcast_file(self, filename):
'''Generates an RSS feed and write the resulting XML to a file.
:param filename: Name of file to write.
'''
feed, doc = self.__create_podcast()
with open(filename, 'w') as f:
doc.write(f)
def itunes_author(self, itunes_author=None): def itunes_author(self, itunes_author=None):
@ -313,20 +288,6 @@ class PodcastGenerator(FeedGenerator):
return self.__itunes_summary return self.__itunes_summary
def add_entry(self, podcastEntry=None):
'''This method will add a new entry to the podcast. If the podcastEntry
argument is omittet a new PodcstEntry object is created automatically.
This is the prefered way to add new entries to a feed.
:param podcastEntry: PodcastEntry object to add.
:returns: PodcastEntry object created or passed to this function.
'''
if podcastEntry is None:
podcastEntry = PodcastEntry()
super(PodcastGenerator, self).add_entry( podcastEntry )
return podcastEntry
_itunes_categories = { _itunes_categories = {
'Arts': [ 'Design', 'Fashion & Beauty', 'Food', 'Literature', 'Arts': [ 'Design', 'Fashion & Beauty', 'Food', 'Literature',
'Performing Arts', 'Visual Arts' ], 'Performing Arts', 'Visual Arts' ],

View file

@ -14,11 +14,11 @@ from lxml import etree
from datetime import datetime from datetime import datetime
import dateutil.parser import dateutil.parser
import dateutil.tz import dateutil.tz
from feedgen.entry import FeedEntry from feedgen.ext.base import BaseExtension
from feedgen.util import ensure_format from feedgen.util import ensure_format
class PodcastEntry(FeedEntry): class PodcastEntryExtension(BaseExtension):
'''FeedEntry extension for podcasts. '''FeedEntry extension for podcasts.
''' '''
@ -36,13 +36,11 @@ class PodcastEntry(FeedEntry):
__itunes_summary = None __itunes_summary = None
def rss_entry(self, feed): def extend_rss(self, entry):
'''Insert an RSS item into a existing XML structure. Normally you '''Add additional fields to an RSS item.
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. :param feed: The RSS item XML element to use.
''' '''
entry = super(PodcastEntry,self).rss_entry(feed)
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
if self.__itunes_author: if self.__itunes_author:

View file

@ -69,9 +69,12 @@ class FeedGenerator(object):
__rss_ttl = None __rss_ttl = None
__rss_webMaster = None __rss_webMaster = None
# Extension list:
__extensions = {}
def _create_atom(self):
def _create_atom(self, extensions=True):
'''Create a ATOM feed xml structure containing all previously set fields. '''Create a ATOM feed xml structure containing all previously set fields.
:returns: Tuple containing the feed root element and the element tree. :returns: Tuple containing the feed root element and the element tree.
@ -81,7 +84,6 @@ class FeedGenerator(object):
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
doc = etree.ElementTree(feed)
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(feed, 'id') id = etree.SubElement(feed, 'id')
@ -165,41 +167,50 @@ class FeedGenerator(object):
subtitle = etree.SubElement(feed, 'subtitle') subtitle = etree.SubElement(feed, 'subtitle')
subtitle.text = self.__atom_subtitle subtitle.text = self.__atom_subtitle
if extensions:
for ext in self.__extensions.values() or []:
if ext.get('atom'):
feed = ext['inst'].extend_atom(feed)
for entry in self.__feed_entries: for entry in self.__feed_entries:
entry.atom_entry(feed) entry.atom_entry(feed)
doc = etree.ElementTree(feed)
return feed, doc return feed, doc
def atom_str(self, pretty=False): 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
generation (default: enabled).
:returns: String representation of the ATOM feed. :returns: String representation of the ATOM feed.
''' '''
feed, doc = self._create_atom() feed, doc = self._create_atom(extensions=extensions)
return etree.tostring(feed, pretty_print=pretty) return etree.tostring(feed, pretty_print=pretty)
def atom_file(self, filename): def atom_file(self, filename, extensions=True):
'''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. :param filename: Name of file to write.
:param extensions: Enable or disable the loaded extensions for the xml
generation (default: enabled).
''' '''
feed, doc = self._create_atom() feed, doc = self._create_atom(extensions=extensions)
with open(filename, 'w') as f: with open(filename, 'w') as f:
doc.write(f) doc.write(f)
def _create_rss(self): def _create_rss(self, extensions=True):
'''Create an RSS feed xml structure containing all previously set fields. '''Create an RSS feed xml structure containing all previously set fields.
: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', feed = etree.Element('rss', version='2.0',
nsmap={'atom': 'http://www.w3.org/2005/Atom'} ) nsmap={'atom': 'http://www.w3.org/2005/Atom'} )
doc = etree.ElementTree(feed)
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 ):
raise ValueError('Required fields not set') raise ValueError('Required fields not set')
@ -306,29 +317,39 @@ class FeedGenerator(object):
webMaster = etree.SubElement(channel, 'webMaster') webMaster = etree.SubElement(channel, 'webMaster')
webMaster.text = self.__rss_webMaster webMaster.text = self.__rss_webMaster
if extensions:
for ext in self.__extensions.values() or []:
if ext.get('rss'):
feed = ext['inst'].extend_rss(feed)
for entry in self.__feed_entries: for entry in self.__feed_entries:
entry.rss_entry(channel) entry.rss_entry(channel)
doc = etree.ElementTree(feed)
return feed, doc return feed, doc
def rss_str(self, pretty=False): 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
generation (default: enabled).
:returns: String representation of the RSS feed. :returns: String representation of the RSS feed.
''' '''
feed, doc = self._create_rss() feed, doc = self._create_rss(extensions=extensions)
return etree.tostring(feed, pretty_print=pretty) return etree.tostring(feed, pretty_print=pretty)
def rss_file(self, filename): def rss_file(self, filename, extensions=True):
'''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. :param filename: Name of file to write.
:param extensions: Enable or disable the loaded extensions for the xml
generation (default: enabled).
''' '''
feed, doc = self._create_rss() feed, doc = self._create_rss(extensions=extensions)
with open(filename, 'w') as f: with open(filename, 'w') as f:
doc.write(f) doc.write(f)
@ -916,6 +937,14 @@ class FeedGenerator(object):
''' '''
if feedEntry is None: if feedEntry is None:
feedEntry = FeedEntry() feedEntry = FeedEntry()
# Try to load extensions:
for extname,ext in self.__extensions.iteritems():
try:
feedEntry.load_extension( extname, ext['atom'], ext['rss'] )
except ImportError:
pass
self.__feed_entries.append( feedEntry ) self.__feed_entries.append( feedEntry )
return feedEntry return feedEntry
@ -942,6 +971,16 @@ class FeedGenerator(object):
entry = [entry] entry = [entry]
if replace: if replace:
self.__feed_entries = [] self.__feed_entries = []
# Try to load extensions:
for e in entry:
for extname,ext in self.__extensions.iteritems():
try:
e.load_extension( extname, ext['atom'], ext['rss'] )
except ImportError:
pass
self.__feed_entries += entry self.__feed_entries += entry
return self.__feed_entries return self.__feed_entries
@ -969,3 +1008,33 @@ class FeedGenerator(object):
remove_entry. remove_entry.
''' '''
self.remove_entry(item) self.remove_entry(item)
def load_extension(self, name, atom=True, rss=True):
'''Load a specific extension by name.
:param name: Name of the extension to load.
:param atom: If the extension should be used for ATOM feeds.
:param rss: If the extension should be used for RSS feeds.
'''
# Check loaded extensions
if not isinstance(self.__extensions, dict):
self.__extensions = {}
if name in self.__extensions.keys():
raise ImportError('Extension already loaded')
# Load extension
extname = name[0].upper() + name[1:] + 'Extension'
supmod = __import__('feedgen.ext.%s' % name)
extmod = getattr(supmod.ext, name)
ext = getattr(extmod, extname)
extinst = ext()
setattr(self, name, extinst)
self.__extensions[name] = {'inst':extinst,'atom':atom,'rss':rss}
# Try to load the extension for already existing entries:
for entry in self.__feed_entries:
try:
entry.load_extension( name, atom, rss )
except ImportError:
pass

0
setup.py Normal file → Executable file
View file