From 840dc7e5b81a6363cc420064f102a745ca35e152 Mon Sep 17 00:00:00 2001 From: Lars Kiesow Date: Sun, 5 May 2013 18:17:44 +0200 Subject: [PATCH] Added extension support and converted PodcastGenerator to extension --- feedgen/__main__.py | 26 +++++---- feedgen/entry.py | 49 +++++++++++++++- feedgen/ext/__init__.py | 6 ++ feedgen/ext/base.py | 39 +++++++++++++ feedgen/{ => ext}/podcast.py | 53 +++-------------- feedgen/{ => ext}/podcast_entry.py | 12 ++-- feedgen/feed.py | 93 ++++++++++++++++++++++++++---- setup.py | 0 8 files changed, 200 insertions(+), 78 deletions(-) create mode 100644 feedgen/ext/__init__.py create mode 100644 feedgen/ext/base.py rename feedgen/{ => ext}/podcast.py (88%) rename feedgen/{ => ext}/podcast_entry.py (96%) mode change 100644 => 100755 setup.py diff --git a/feedgen/__main__.py b/feedgen/__main__.py index 9f6b26b..1c73f94 100644 --- a/feedgen/__main__.py +++ b/feedgen/__main__.py @@ -9,7 +9,6 @@ ''' from feedgen.feed import FeedGenerator -from feedgen.podcast import PodcastGenerator import sys @@ -32,7 +31,7 @@ if __name__ == '__main__': arg = sys.argv[1] - fg = PodcastGenerator() if arg.endswith('podcast') else FeedGenerator() + fg = FeedGenerator() fg.id('http://lernfunk.de/_MEDIAID_123') fg.title('Testfeed') fg.author( {'name':'Lars Kiesow','email':'lkiesow@uos.de'} ) @@ -64,16 +63,21 @@ if __name__ == '__main__': elif arg == 'rss': print fg.rss_str(pretty=True) elif arg == 'podcast': - fg.itunes_author('Lars Kiesow') - fg.itunes_category('Technology', 'Podcasting') - fg.itunes_explicit('no') - fg.itunes_complete('no') - fg.itunes_new_feed_url('http://example.com/new-feed.rss') - fg.itunes_owner('John Doe', 'john@example.com') - fg.itunes_summary('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Verba tu fingas et ea dicas, quae non sentias?') - fe.itunes_author('Lars Kiesow') + # Load the podcast extension. It will automatically be loaded for all + # entries in the feed, too. Thus also for our “fe”. + fg.load_extension('podcast') + fg.podcast.itunes_author('Lars Kiesow') + fg.podcast.itunes_category('Technology', 'Podcasting') + fg.podcast.itunes_explicit('no') + fg.podcast.itunes_complete('no') + 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'): fg.atom_file(arg) elif arg.endswith('rss'): diff --git a/feedgen/entry.py b/feedgen/entry.py index 3a440cd..0283e9d 100644 --- a/feedgen/entry.py +++ b/feedgen/entry.py @@ -50,8 +50,11 @@ class FeedEntry(object): __rss_source = 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 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.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 - def rss_entry(self, feed): + 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. @@ -184,6 +192,12 @@ class FeedEntry(object): pubDate = etree.SubElement(channel, 'pubDate') pubDate.text = self.__rss_pubDate.strftime( '%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 @@ -575,3 +589,34 @@ class FeedEntry(object): if not ttl is None: self.__rss_ttl = int(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} diff --git a/feedgen/ext/__init__.py b/feedgen/ext/__init__.py new file mode 100644 index 0000000..a3dd0f9 --- /dev/null +++ b/feedgen/ext/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +""" + =========== + feedgen.ext + =========== +""" diff --git a/feedgen/ext/base.py b/feedgen/ext/base.py new file mode 100644 index 0000000..4c40f7b --- /dev/null +++ b/feedgen/ext/base.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +''' + feedgen.ext.base + ~~~~~~~~~~~~~~~~ + + Basic FeedGenerator which does nothing but provides all necessary methods. + + :copyright: 2013, Lars Kiesow + + :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. + ''' diff --git a/feedgen/podcast.py b/feedgen/ext/podcast.py similarity index 88% rename from feedgen/podcast.py rename to feedgen/ext/podcast.py index 962a697..812616a 100644 --- a/feedgen/podcast.py +++ b/feedgen/ext/podcast.py @@ -14,12 +14,11 @@ from lxml import etree from datetime import datetime import dateutil.parser import dateutil.tz -from feedgen.feed import FeedGenerator -from feedgen.podcast_entry import PodcastEntry +from feedgen.ext.base import BaseExtension from feedgen.util import ensure_format -class PodcastGenerator(FeedGenerator): +class PodcastExtension(BaseExtension): '''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. :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' # Replace the root element to add the itunes namespace - feed = etree.Element('rss', version='2.0', - nsmap={ - 'atom' :'http://www.w3.org/2005/Atom', - 'itunes':ITUNES_NS} ) + nsmap = rss_feed.nsmap + nsmap['itunes'] = ITUNES_NS + feed = etree.Element('rss', version='2.0', nsmap=nsmap ) feed[:] = rss_feed[:] channel = feed[0] - doc = etree.ElementTree(feed) if self.__itunes_author: author = etree.SubElement(channel, '{%s}author' % ITUNES_NS) @@ -102,28 +98,7 @@ class PodcastGenerator(FeedGenerator): summary = etree.SubElement(channel, '{%s}summary' % ITUNES_NS) summary.text = self.__itunes_summary - return feed, doc - - - 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) + return feed def itunes_author(self, itunes_author=None): @@ -313,20 +288,6 @@ class PodcastGenerator(FeedGenerator): 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 = { 'Arts': [ 'Design', 'Fashion & Beauty', 'Food', 'Literature', 'Performing Arts', 'Visual Arts' ], diff --git a/feedgen/podcast_entry.py b/feedgen/ext/podcast_entry.py similarity index 96% rename from feedgen/podcast_entry.py rename to feedgen/ext/podcast_entry.py index 7e8ba55..cd20d69 100644 --- a/feedgen/podcast_entry.py +++ b/feedgen/ext/podcast_entry.py @@ -14,11 +14,11 @@ from lxml import etree from datetime import datetime import dateutil.parser import dateutil.tz -from feedgen.entry import FeedEntry +from feedgen.ext.base import BaseExtension from feedgen.util import ensure_format -class PodcastEntry(FeedEntry): +class PodcastEntryExtension(BaseExtension): '''FeedEntry extension for podcasts. ''' @@ -36,13 +36,11 @@ class PodcastEntry(FeedEntry): __itunes_summary = None - def rss_entry(self, feed): - '''Insert an RSS item into a existing XML structure. Normally you - would pass the channel node of an RSS feed XML to this function. + def extend_rss(self, entry): + '''Add additional fields to an RSS item. - :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' if self.__itunes_author: diff --git a/feedgen/feed.py b/feedgen/feed.py index 19c9cd1..7a9b62e 100644 --- a/feedgen/feed.py +++ b/feedgen/feed.py @@ -69,9 +69,12 @@ class FeedGenerator(object): __rss_ttl = 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. :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'] = \ self.__atom_feed_xml_lang - doc = etree.ElementTree(feed) if not ( self.__atom_id and self.__atom_title and self.__atom_updated ): raise ValueError('Required fields not set') id = etree.SubElement(feed, 'id') @@ -165,41 +167,50 @@ class FeedGenerator(object): subtitle = etree.SubElement(feed, '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: entry.atom_entry(feed) + doc = etree.ElementTree(feed) 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. :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 + generation (default: enabled). :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) - def atom_file(self, filename): + def atom_file(self, filename, extensions=True): '''Generates an ATOM feed and write the resulting XML to a file. :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: doc.write(f) - def _create_rss(self): + def _create_rss(self, extensions=True): '''Create an RSS feed xml structure containing all previously set fields. :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'} ) - doc = etree.ElementTree(feed) channel = etree.SubElement(feed, 'channel') if not ( self.__rss_title and self.__rss_link and self.__rss_description ): raise ValueError('Required fields not set') @@ -306,29 +317,39 @@ class FeedGenerator(object): webMaster = etree.SubElement(channel, '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: entry.rss_entry(channel) + doc = etree.ElementTree(feed) 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. :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 + generation (default: enabled). :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) - def rss_file(self, filename): + def rss_file(self, filename, extensions=True): '''Generates an RSS feed and write the resulting XML to a file. :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: doc.write(f) @@ -916,6 +937,14 @@ class FeedGenerator(object): ''' if feedEntry is None: 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 ) return feedEntry @@ -942,6 +971,16 @@ class FeedGenerator(object): entry = [entry] if replace: 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 return self.__feed_entries @@ -969,3 +1008,33 @@ class FeedGenerator(object): remove_entry. ''' 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 diff --git a/setup.py b/setup.py old mode 100644 new mode 100755