From 4a254b719b88e37cd1eb2fc18142ddc6e90aa03a Mon Sep 17 00:00:00 2001 From: Dannieboy Date: Thu, 31 Dec 2020 23:54:30 +0100 Subject: [PATCH] Revert "Made the plugin less strict while still adhering to the atom standart." This reverts commit 0c5abfb2ccaf1b154cbaa414fdae86352034b319. --- __init__.py | 137 ----- __main__.py | 145 ------ compat.py | 8 - entry.py | 738 -------------------------- ext/__init__.py | 6 - ext/base.py | 43 -- ext/dc.py | 407 --------------- ext/geo.py | 21 - ext/geo_entry.py | 329 ------------ ext/media.py | 183 ------- ext/podcast.py | 355 ------------- ext/podcast_entry.py | 244 --------- ext/syndication.py | 60 --- ext/torrent.py | 126 ----- feed.py | 1169 ------------------------------------------ util.py | 96 ---- version.py | 25 - 17 files changed, 4092 deletions(-) delete mode 100644 __init__.py delete mode 100644 __main__.py delete mode 100644 compat.py delete mode 100644 entry.py delete mode 100644 ext/__init__.py delete mode 100644 ext/base.py delete mode 100644 ext/dc.py delete mode 100644 ext/geo.py delete mode 100644 ext/geo_entry.py delete mode 100644 ext/media.py delete mode 100644 ext/podcast.py delete mode 100644 ext/podcast_entry.py delete mode 100644 ext/syndication.py delete mode 100644 ext/torrent.py delete mode 100644 feed.py delete mode 100644 util.py delete mode 100644 version.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 624658e..0000000 --- a/__init__.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -""" - ======= - feedgen - ======= - - This module can be used to generate web feeds in both ATOM and RSS format. - It has support for extensions. Included is for example an extension to - produce Podcasts. - - :copyright: 2013 by Lars Kiesow - :license: FreeBSD and LGPL, see license.* for more details. - - - ------------- - Create a Feed - ------------- - - To create a feed simply instantiate the FeedGenerator class and insert some - data:: - - >>> from feedgen.feed import FeedGenerator - >>> fg = FeedGenerator() - >>> fg.id('http://lernfunk.de/media/654321') - >>> fg.title('Some Testfeed') - >>> fg.author( {'name':'John Doe','email':'john@example.de'} ) - >>> fg.link( href='http://example.com', rel='alternate' ) - >>> fg.logo('http://ex.com/logo.jpg') - >>> fg.subtitle('This is a cool feed!') - >>> fg.link( href='http://larskiesow.de/test.atom', rel='self' ) - >>> fg.language('en') - - Note that for the methods which set fields that can occur more than once in - a feed you can use all of the following ways to provide data: - - - Provide the data for that element as keyword arguments - - Provide the data for that element as dictionary - - Provide a list of dictionaries with the data for several elements - - Example:: - - >>> fg.contributor(name='John Doe', email='jdoe@example.com' ) - >>> fg.contributor({'name':'John Doe', 'email':'jdoe@example.com'}) - >>> fg.contributor([{'name':'John', 'email':'jdoe@example.com'}, …]) - - ----------------- - Generate the Feed - ----------------- - - After that you can generate both RSS or ATOM by calling the respective - method:: - - >>> atomfeed = fg.atom_str(pretty=True) # Get the ATOM feed as string - >>> rssfeed = fg.rss_str(pretty=True) # Get the RSS feed as string - >>> fg.atom_file('atom.xml') # Write the ATOM feed to a file - >>> fg.rss_file('rss.xml') # Write the RSS feed to a file - - - ---------------- - Add Feed Entries - ---------------- - - To add entries (items) to a feed you need to create new FeedEntry objects - and append them to the list of entries in the FeedGenerator. The most - convenient way to go is to use the FeedGenerator itself for the - instantiation of the FeedEntry object:: - - >>> fe = fg.add_entry() - >>> fe.id('http://lernfunk.de/media/654321/1') - >>> fe.title('The First Episode') - - The FeedGenerators method add_entry(...) without argument provides will - automatically generate a new FeedEntry object, append it to the feeds - internal list of entries and return it, so that additional data can be - added. - - ---------- - Extensions - ---------- - - The FeedGenerator supports extension to include additional data into the - XML structure of the feeds. Extensions can be loaded like this:: - - >>> fg.load_extension('someext', atom=True, rss=True) - - This will try to load the extension “someext” from the file - `ext/someext.py`. It is required that `someext.py` contains a class named - “SomextExtension” which is required to have at least the two methods - `extend_rss(...)` and `extend_atom(...)`. Although not required, it is - strongly suggested to use BaseExtension from `ext/base.py` as superclass. - - `load_extension('someext', ...)` will also try to load a class named - “SomextEntryExtension” for every entry of the feed. This class can be - located either in the same file as SomextExtension or in - `ext/someext_entry.py` which is suggested especially for large extensions. - - The parameters `atom` and `rss` tell the FeedGenerator if the extensions - should only be used for either ATOM or RSS feeds. The default value for - both parameters is true which means that the extension would be used for - both kinds of feeds. - - **Example: Producing a Podcast** - - One extension already provided is the podcast extension. A podcast is an - RSS feed with some additional elements for ITunes. - - To produce a podcast simply load the `podcast` extension:: - - >>> from feedgen.feed import FeedGenerator - >>> fg = FeedGenerator() - >>> fg.load_extension('podcast') - ... - >>> fg.podcast.itunes_category('Technology', 'Podcasting') - ... - >>> fg.rss_str(pretty=True) - >>> fg.rss_file('podcast.xml') - - Of cause the extension has to be loaded for the FeedEntry objects as well - but this is done automatically by the FeedGenerator for every feed entry if - the extension is loaded for the whole feed. You can, however, load an - extension for a specific FeedEntry by calling `load_extension(...)` on that - entry. But this is a rather uncommon use. - - Of cause you can still produce a normal ATOM or RSS feed, even if you have - loaded some plugins by temporary disabling them during the feed generation. - This can be done by calling the generating method with the keyword argument - `extensions` set to `False`. - - --------------------- - Testing the Generator - --------------------- - - You can test the module by simply executing:: - - $ python -m feedgen - -""" diff --git a/__main__.py b/__main__.py deleted file mode 100644 index abc0737..0000000 --- a/__main__.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen - ~~~~~~~ - - :copyright: 2013-2016, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -import sys - -from feedgen.feed import FeedGenerator - - -USAGE = ''' -Usage: python -m feedgen [OPTION] - -Use one of the following options: - -File options: - .atom -- Generate ATOM test feed - .rss -- Generate RSS test teed - -Stdout options: - atom -- Generate ATOM test output - rss -- Generate RSS test output - podcast -- Generate Podcast test output - dc.atom -- Generate DC extension test output (atom format) - dc.rss -- Generate DC extension test output (rss format) - syndication.atom -- Generate syndication extension test output (atom format) - syndication.rss -- Generate syndication extension test output (rss format) - torrent -- Generate Torrent test output - -''' - - -def print_enc(s): - '''Print function compatible with both python2 and python3 accepting strings - and byte arrays. - ''' - if sys.version_info[0] >= 3: - print(s.decode('utf-8') if isinstance(s, bytes) else s) - else: - print(s) - - -def main(): - if len(sys.argv) != 2 or not ( - sys.argv[1].endswith('rss') or - sys.argv[1].endswith('atom') or - sys.argv[1] == 'torrent' or - sys.argv[1] == 'podcast'): - print(USAGE) - exit() - - arg = sys.argv[1] - - fg = FeedGenerator() - fg.id('http://lernfunk.de/_MEDIAID_123') - fg.title('Testfeed') - fg.author({'name': 'Lars Kiesow', 'email': 'lkiesow@uos.de'}) - fg.link(href='http://example.com', rel='alternate') - fg.category(term='test') - fg.contributor(name='Lars Kiesow', email='lkiesow@uos.de') - fg.contributor(name='John Doe', email='jdoe@example.com') - fg.icon('http://ex.com/icon.jpg') - fg.logo('http://ex.com/logo.jpg') - fg.rights('cc-by') - fg.subtitle('This is a cool feed!') - fg.link(href='http://larskiesow.de/test.atom', rel='self') - fg.language('de') - fe = fg.add_entry() - fe.id('http://lernfunk.de/_MEDIAID_123#1') - fe.title('First Element') - fe.content('''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tamen - aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si - ista mala sunt, placet. Aut etiam, ut vestitum, sic sententiam - habeas aliam domesticam, aliam forensem, ut in fronte ostentatio - sit, intus veritas occultetur? Cum id fugiunt, re eadem defendunt, - quae Peripatetici, verba.''') - fe.summary(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit…') - fe.link(href='http://example.com', rel='alternate') - fe.author(name='Lars Kiesow', email='lkiesow@uos.de') - - if arg == 'atom': - print_enc(fg.atom_str(pretty=True)) - elif arg == 'rss': - print_enc(fg.rss_str(pretty=True)) - elif arg == 'podcast': - # 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_enc(fg.rss_str(pretty=True)) - - elif arg == 'torrent': - fg.load_extension('torrent') - fe.link(href='http://example.com/torrent/debian-8-netint.iso.torrent', - rel='alternate', - type='application/x-bittorrent, length=1000') - fe.torrent.filename('debian-8.4.0-i386-netint.iso.torrent') - fe.torrent.infohash('7661229811ef32014879ceedcdf4a48f256c88ba') - fe.torrent.contentlength('331350016') - fe.torrent.seeds('789') - fe.torrent.peers('456') - fe.torrent.verified('123') - print_enc(fg.rss_str(pretty=True)) - - elif arg.startswith('dc.'): - fg.load_extension('dc') - fg.dc.dc_contributor('Lars Kiesow') - if arg.endswith('.atom'): - print_enc(fg.atom_str(pretty=True)) - else: - print_enc(fg.rss_str(pretty=True)) - - elif arg.startswith('syndication'): - fg.load_extension('syndication') - fg.syndication.update_period('daily') - fg.syndication.update_frequency(2) - fg.syndication.update_base('2000-01-01T12:00+00:00') - if arg.endswith('.rss'): - print_enc(fg.rss_str(pretty=True)) - else: - print_enc(fg.atom_str(pretty=True)) - - elif arg.endswith('atom'): - fg.atom_file(arg) - - elif arg.endswith('rss'): - fg.rss_file(arg) - - -if __name__ == '__main__': - main() diff --git a/compat.py b/compat.py deleted file mode 100644 index e9044b0..0000000 --- a/compat.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - -import sys - -if sys.version_info[0] >= 3: - string_types = str -else: - string_types = basestring # noqa: F821 diff --git a/entry.py b/entry.py deleted file mode 100644 index 66400ba..0000000 --- a/entry.py +++ /dev/null @@ -1,738 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.entry - ~~~~~~~~~~~~~ - - :copyright: 2013-2020, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -from datetime import datetime - -import dateutil.parser -import dateutil.tz -import warnings - -from lxml.etree import CDATA # nosec - adding CDATA entry is safe - -from feedgen.compat import string_types -from feedgen.util import ensure_format, formatRFC2822, xml_fromstring, xml_elem - - -def _add_text_elm(entry, data, name): - """Add a text subelement to an entry""" - if not data: - return - - elm = xml_elem(name, entry) - type_ = data.get('type') - if data.get('src'): - if name != 'content': - raise ValueError("Only the 'content' element of an entry can " - "contain a 'src' attribute") - elm.attrib['src'] = data['src'] - elif data.get(name): - # Surround xhtml with a div tag, parse it and embed it - if type_ == 'xhtml': - xhtml = '
' \ - + data.get(name) + '
' - elm.append(xml_fromstring(xhtml)) - elif type_ == 'CDATA': - elm.text = CDATA(data.get(name)) - # Parse XML and embed it - elif type_ and (type_.endswith('/xml') or type_.endswith('+xml')): - elm.append(xml_fromstring(data[name])) - # Embed the text in escaped form - elif not type_ or type_.startswith('text') or type_ == 'html': - elm.text = data.get(name) - # Everything else should be included base64 encoded - else: - raise NotImplementedError( - 'base64 encoded {} is not supported at the moment. ' - 'Pull requests adding support are welcome.'.format(name) - ) - # Add type description of the content - if type_: - elm.attrib['type'] = type_ - - -class FeedEntry(object): - '''FeedEntry call representing an ATOM feeds entry node or an RSS feeds item - node. - ''' - - def __init__(self): - # ATOM - # required - self.__atom_id = None - self.__atom_title = None - self.__atom_updated = datetime.now(dateutil.tz.tzutc()) - - # recommended - self.__atom_author = None - self.__atom_content = None - self.__atom_link = None - self.__atom_summary = None - - # optional - self.__atom_category = None - self.__atom_contributor = None - self.__atom_published = None - self.__atom_source = None - self.__atom_rights = None - - # RSS - self.__rss_author = None - self.__rss_category = None - self.__rss_comments = None - self.__rss_description = None - self.__rss_content = None - self.__rss_enclosure = None - self.__rss_guid = {} - self.__rss_link = None - self.__rss_pubDate = None - self.__rss_source = None - self.__rss_title = None - - # Extension list: - self.__extensions = {} - self.__extensions_register = {} - - def atom_entry(self, extensions=True): - '''Create an ATOM entry and return it.''' - entry = xml_elem('entry') - if not (self.__atom_id and self.__atom_title and self.__atom_updated): - raise ValueError('Required fields not set') - id = xml_elem('id', entry) - id.text = self.__atom_id - title = xml_elem('title', entry) - title.text = self.__atom_title - updated = xml_elem('updated', entry) - updated.text = self.__atom_updated.isoformat() - - # An entry must contain an alternate link if there is no content - # element. - if not self.__atom_content: - links = self.__atom_link or [] - if not [l for l in links if l.get('rel') == 'alternate']: - raise ValueError('Entry must contain an alternate link or ' + - 'a content element.') - - # Add author elements - for a in self.__atom_author or []: - # Atom requires a name. Skip elements without. - if not a.get('name'): - continue - author = xml_elem('author', entry) - name = xml_elem('name', author) - name.text = a.get('name') - if a.get('email'): - email = xml_elem('email', author) - email.text = a.get('email') - if a.get('uri'): - uri = xml_elem('uri', author) - uri.text = a.get('uri') - - _add_text_elm(entry, self.__atom_content, 'content') - - for l in self.__atom_link or []: - link = xml_elem('link', entry, href=l['href']) - if l.get('rel'): - link.attrib['rel'] = l['rel'] - if l.get('type'): - link.attrib['type'] = l['type'] - if l.get('hreflang'): - link.attrib['hreflang'] = l['hreflang'] - if l.get('title'): - link.attrib['title'] = l['title'] - if l.get('length'): - link.attrib['length'] = l['length'] - - _add_text_elm(entry, self.__atom_summary, 'summary') - - for c in self.__atom_category or []: - cat = xml_elem('category', entry, term=c['term']) - if c.get('scheme'): - cat.attrib['scheme'] = c['scheme'] - if c.get('label'): - cat.attrib['label'] = c['label'] - - # Add author elements - for c in self.__atom_contributor or []: - # Atom requires a name. Skip elements without. - if not c.get('name'): - continue - contrib = xml_elem('contributor', entry) - name = xml_elem('name', contrib) - name.text = c.get('name') - if c.get('email'): - email = xml_elem('email', contrib) - email.text = c.get('email') - if c.get('uri'): - uri = xml_elem('uri', contrib) - uri.text = c.get('uri') - - if self.__atom_published: - published = xml_elem('published', entry) - published.text = self.__atom_published.isoformat() - - if self.__atom_rights: - rights = xml_elem('rights', entry) - rights.text = self.__atom_rights - - if self.__atom_source: - source = xml_elem('source', entry) - if self.__atom_source.get('title'): - source_title = xml_elem('title', source) - source_title.text = self.__atom_source['title'] - if self.__atom_source.get('link'): - xml_elem('link', source, href=self.__atom_source['link']) - - if extensions: - for ext in self.__extensions.values() or []: - if ext.get('atom'): - ext['inst'].extend_atom(entry) - - return entry - - def rss_entry(self, extensions=True): - '''Create a RSS item and return it.''' - entry = xml_elem('item') - if not (self.__rss_title or - self.__rss_description or - self.__rss_content): - raise ValueError('Required fields not set') - if self.__rss_title: - title = xml_elem('title', entry) - title.text = self.__rss_title - if self.__rss_link: - link = xml_elem('link', entry) - link.text = self.__rss_link - if self.__rss_description and self.__rss_content: - description = xml_elem('description', entry) - description.text = self.__rss_description - XMLNS_CONTENT = 'http://purl.org/rss/1.0/modules/content/' - content = xml_elem('{%s}encoded' % XMLNS_CONTENT, entry) - content.text = CDATA(self.__rss_content['content']) \ - if self.__rss_content.get('type', '') == 'CDATA' \ - else self.__rss_content['content'] - elif self.__rss_description: - description = xml_elem('description', entry) - description.text = self.__rss_description - elif self.__rss_content: - description = xml_elem('description', entry) - description.text = CDATA(self.__rss_content['content']) \ - if self.__rss_content.get('type', '') == 'CDATA' \ - else self.__rss_content['content'] - for a in self.__rss_author or []: - author = xml_elem('author', entry) - author.text = a - if self.__rss_guid.get('guid'): - guid = xml_elem('guid', entry) - guid.text = self.__rss_guid['guid'] - permaLink = str(self.__rss_guid.get('permalink', False)).lower() - guid.attrib['isPermaLink'] = permaLink - for cat in self.__rss_category or []: - category = xml_elem('category', entry) - category.text = cat['value'] - if cat.get('domain'): - category.attrib['domain'] = cat['domain'] - if self.__rss_comments: - comments = xml_elem('comments', entry) - comments.text = self.__rss_comments - if self.__rss_enclosure: - enclosure = xml_elem('enclosure', entry) - enclosure.attrib['url'] = self.__rss_enclosure['url'] - enclosure.attrib['length'] = self.__rss_enclosure['length'] - enclosure.attrib['type'] = self.__rss_enclosure['type'] - if self.__rss_pubDate: - pubDate = xml_elem('pubDate', entry) - pubDate.text = formatRFC2822(self.__rss_pubDate) - if self.__rss_source: - source = xml_elem('source', entry, url=self.__rss_source['url']) - source.text = self.__rss_source['title'] - - if extensions: - for ext in self.__extensions.values() or []: - if ext.get('rss'): - ext['inst'].extend_rss(entry) - - 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 - and should not be blank. - - :param title: The new title of the entry. - :returns: The entriess title. - ''' - if title is not None: - self.__atom_title = title - self.__rss_title = title - return self.__atom_title - - def id(self, id=None): - '''Get or set the entry id which identifies the entry using a - universally unique and permanent URI. Two entries in a feed can have - the same value for id if they represent the same entry at different - points in time. This method will also set rss:guid with permalink set - to False. Id is mandatory for an ATOM entry. - - :param id: New Id of the entry. - :returns: Id of the entry. - ''' - if id is not None: - self.__atom_id = id - self.__rss_guid = {'guid': id, 'permalink': False} - return self.__atom_id - - def guid(self, guid=None, permalink=False): - '''Get or set the entries guid which is a string that uniquely - identifies the item. This will also set atom:id. - - :param guid: Id of the entry. - :param permalink: If this is a permanent identifier for this item - :returns: Id and permalink setting of the entry. - ''' - if guid is not None: - self.__atom_id = guid - self.__rss_guid = {'guid': guid, 'permalink': permalink} - return self.__rss_guid - - def updated(self, updated=None): - '''Set or get the updated value which indicates the last time the entry - was modified in a significant way. - - The value can either be a string which will automatically be parsed or - a datetime.datetime object. In any case it is necessary that the value - include timezone information. - - :param updated: The modification date. - :returns: Modification date as datetime.datetime - ''' - if updated is not None: - if isinstance(updated, string_types): - updated = dateutil.parser.parse(updated) - if not isinstance(updated, datetime): - raise ValueError('Invalid datetime format') - if updated.tzinfo is None: - raise ValueError('Datetime object has no timezone info') - self.__atom_updated = updated - self.__rss_lastBuildDate = updated - - return self.__atom_updated - - def author(self, author=None, replace=False, **kwargs): - '''Get or set author data. An author element is a dict containing a - name, an email address and a uri. Name is mandatory for ATOM, email is - mandatory for RSS. - - This method can be called with: - - the fields of an author as keyword arguments - - the fields of an author as a dictionary - - a list of dictionaries containing the author fields - - An author has the following fields: - - *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. - - Example:: - - >>> author({'name':'John Doe', 'email':'jdoe@example.com'}) - [{'name':'John Doe','email':'jdoe@example.com'}] - - >>> author([{'name': 'Mr. X'}, {'name': 'Max'}]) - [{'name':'John Doe','email':'jdoe@example.com'}, - {'name':'John Doe'}, {'name':'Max'}] - - >>> author(name='John Doe', email='jdoe@example.com', replace=True) - [{'name':'John Doe','email':'jdoe@example.com'}] - - ''' - if author is None and kwargs: - author = kwargs - if author is not None: - if replace or self.__atom_author is None: - self.__atom_author = [] - self.__atom_author += ensure_format(author, - set(['name', 'email', 'uri']), - set()) - self.__rss_author = [] - for a in self.__atom_author: - if a.get('email'): - if a.get('name'): - self.__rss_author.append('%(email)s (%(name)s)' % a) - else: - self.__rss_author.append('%(email)s' % a) - return self.__atom_author - - def content(self, content=None, src=None, type=None): - '''Get or set the content of the entry which contains or links to the - complete content of the entry. Content must be provided for ATOM - entries if there is no alternate link, and should be provided if there - is no summary. If the content is set (not linked) it will also set - rss:description. - - :param content: The content of the feed entry. - :param src: Link to the entries content. - :param type: If type is CDATA content would not be escaped. - :returns: Content element of the entry. - ''' - if src is not None: - self.__atom_content = {'src': src} - elif content is not None: - self.__atom_content = {'content': content} - self.__rss_content = {'content': content} - if type is not None: - self.__atom_content['type'] = type - self.__rss_content['type'] = type - return self.__atom_content - - def link(self, link=None, replace=False, **kwargs): - '''Get or set link data. An link element is a dict with the fields - href, rel, type, hreflang, title, and length. Href is mandatory for - ATOM. - - This method can be called with: - - the fields of a link as keyword arguments - - the fields of a link as a dictionary - - a list of dictionaries containing the link fields - - A link has the following fields: - - - *href* is the URI of the referenced resource (typically a Web page) - - *rel* contains a single link relationship type. It can be a full URI, - or one of the following predefined values (default=alternate): - - - *alternate* an alternate representation of the entry or feed, for - example a permalink to the html version of the entry, or the - front page of the weblog. - - *enclosure* a related resource which is potentially large in size - and might require special handling, for example an audio or video - recording. - - *related* an document related to the entry or feed. - - *self* the feed itself. - - *via* the source of the information provided in the entry. - - - *type* indicates the media type of the resource. - - *hreflang* indicates the language of the referenced resource. - - *title* human readable information about the link, typically for - display purposes. - - *length* the length of the resource, in bytes. - - RSS only supports one link with nothing but a URL. So for the RSS link - element the last link with rel=alternate is used. - - 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. - ''' - if link is None and kwargs: - link = kwargs - if link is not None: - if replace or self.__atom_link is None: - self.__atom_link = [] - self.__atom_link += ensure_format( - link, - set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), - set(['href']), - {'rel': ['alternate', 'enclosure', 'related', 'self', 'via']}, - {'rel': 'alternate'}) - # RSS only needs one URL. We use the first link for RSS: - for l in self.__atom_link: - if l.get('rel') == 'alternate': - self.__rss_link = l['href'] - elif l.get('rel') == 'enclosure': - self.__rss_enclosure = {'url': l['href']} - self.__rss_enclosure['type'] = l.get('type') - self.__rss_enclosure['length'] = l.get('length') or '0' - # return the set with more information (atom) - return self.__atom_link - - def summary(self, summary=None, type=None): - '''Get or set the summary element of an entry which conveys a short - summary, abstract, or excerpt of the entry. Summary is an ATOM only - element and should be provided if there either is no content provided - for the entry, or that content is not inline (i.e., contains a src - attribute), or if the content is encoded in base64. This method will - also set the rss:description field if it wasn't previously set or - contains the old value of summary. - - :param summary: Summary of the entries contents. - :returns: Summary of the entries contents. - ''' - if summary is not None: - # Replace the RSS description with the summary if it was the - # summary before. Not if it is the description. - if not self.__rss_description or ( - self.__atom_summary and - self.__rss_description == self.__atom_summary.get("summary") - ): - self.__rss_description = summary - - self.__atom_summary = {'summary': summary} - if type is not None: - self.__atom_summary['type'] = type - return self.__atom_summary - - def description(self, description=None, isSummary=False): - '''Get or set the description value which is the item synopsis. - Description is an RSS only element. For ATOM feeds it is split in - summary and content. The isSummary parameter can be used to control - which ATOM value is set when setting description. - - :param description: Description of the entry. - :param isSummary: If the description should be used as content or - summary. - :returns: The entries description. - ''' - if description is not None: - self.__rss_description = description - if isSummary: - self.__atom_summary = description - else: - self.__atom_content = {'content': description} - return self.__rss_description - - def category(self, category=None, replace=False, **kwargs): - '''Get or set categories that the entry belongs to. - - This method can be called with: - - the fields of a category as keyword arguments - - the fields of a category as a dictionary - - a list of dictionaries containing the category fields - - A categories has the following fields: - - *term* identifies the category - - *scheme* identifies the categorization scheme via a URI. - - *label* provides a human-readable label for display - - If a label is present it is used for the RSS feeds. Otherwise the term - is used. The scheme is used for the domain attribute in RSS. - - :param category: Dict or list of dicts with data. - :param replace: Add or replace old data. - :returns: List of category data. - ''' - if category is None and kwargs: - category = kwargs - if category is not None: - if replace or self.__atom_category is None: - self.__atom_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 - # name or if not present the atom:term. The atom:scheme is the - # rss:domain. - self.__rss_category = [] - for cat in self.__atom_category: - rss_cat = {} - rss_cat['value'] = cat.get('label', cat['term']) - if cat.get('scheme'): - rss_cat['domain'] = cat['scheme'] - self.__rss_category.append(rss_cat) - return self.__atom_category - - def contributor(self, contributor=None, replace=False, **kwargs): - '''Get or set the contributor data of the feed. This is an ATOM only - value. - - This method can be called with: - - the fields of an contributor as keyword arguments - - the fields of an contributor as a dictionary - - a list of dictionaries containing the contributor fields - - An contributor has the following fields: - - *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. - ''' - if contributor is None and kwargs: - contributor = kwargs - if contributor is not None: - if replace or self.__atom_contributor is None: - self.__atom_contributor = [] - self.__atom_contributor += ensure_format( - contributor, set(['name', 'email', 'uri']), set(['name'])) - return self.__atom_contributor - - def published(self, published=None): - '''Set or get the published value which contains the time of the initial - creation or first availability of the entry. - - The value can either be a string which will automatically be parsed or - a datetime.datetime object. In any case it is necessary that the value - include timezone information. - - :param published: The creation date. - :returns: Creation date as datetime.datetime - ''' - if published is not None: - if isinstance(published, string_types): - published = dateutil.parser.parse(published) - if not isinstance(published, datetime): - raise ValueError('Invalid datetime format') - if published.tzinfo is None: - raise ValueError('Datetime object has no timezone info') - self.__atom_published = published - self.__rss_pubDate = published - - 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(...) - method. - ''' - return self.published(pubDate) - - 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(...) - method. - - pubdate(…) is deprecated and may be removed in feedgen ≥ 0.8. Use - pubDate(…) instead. - ''' - warnings.warn('pubdate(…) is deprecated and may be removed in feedgen ' - '≥ 0.8. Use pubDate(…) instead.') - return self.published(pubDate) - - def rights(self, rights=None): - '''Get or set the rights value of the entry which conveys information - about rights, e.g. copyrights, held in and over the entry. This ATOM - value will also set rss:copyright. - - :param rights: Rights information of the feed. - :returns: Rights information of the feed. - ''' - if rights is not None: - self.__atom_rights = rights - return self.__atom_rights - - def comments(self, comments=None): - '''Get or set the value of comments which is the URL of the comments - page for the item. This is a RSS only value. - - :param comments: URL to the comments page. - :returns: URL to the comments page. - ''' - if comments is not None: - self.__rss_comments = comments - return self.__rss_comments - - def source(self, url=None, title=None): - '''Get or set the source for the current feed entry. - - Note that ATOM feeds support a lot more sub elements than title and URL - (which is what RSS supports) but these are currently not supported. - Patches are welcome. - - :param url: Link to the source. - :param title: Title of the linked resource - :returns: Source element as dictionaries. - ''' - if url is not None and title is not None: - self.__rss_source = {'url': url, 'title': title} - self.__atom_source = {'link': url, 'title': title} - return self.__rss_source - - def enclosure(self, url=None, length=None, type=None): - '''Get or set the value of enclosure which describes a media object - that is attached to the item. This is a RSS only value which is - represented by link(rel=enclosure) in ATOM. ATOM feeds can furthermore - contain several enclosures while RSS may contain only one. That is why - this method, if repeatedly called, will add more than one enclosures to - the feed. However, only the last one is used for RSS. - - :param url: URL of the media object. - :param length: Size of the media in bytes. - :param type: Mimetype of the linked media. - :returns: Data of the enclosure element. - ''' - if url is not None: - self.link(href=url, rel='enclosure', type=type, length=length) - return self.__rss_enclosure - - def ttl(self, ttl=None): - '''Get or set the ttl value. It is an RSS only element. ttl stands for - time to live. It's a number of minutes that indicates how long a - channel can be cached before refreshing from the source. - - :param ttl: Integer value representing the time to live. - :returns: Time to live of of the entry. - ''' - if ttl is not 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: - supmod = __import__('feedgen.ext.%s_entry' % name) - extmod = getattr(supmod.ext, name + '_entry') - except ImportError: - # Use FeedExtension module instead - supmod = __import__('feedgen.ext.%s' % name) - extmod = getattr(supmod.ext, name) - ext = getattr(extmod, extname) - self.register_extension(name, ext, atom, rss) - - def register_extension(self, namespace, extension_class_entry=None, - atom=True, rss=True): - '''Register a specific extension by classes to a namespace. - - :param namespace: namespace for the extension - :param extension_class_entry: Class of the entry 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 - # `load_extension` ignores the "Extension" suffix. - if not isinstance(self.__extensions, dict): - self.__extensions = {} - if namespace in self.__extensions.keys(): - raise ImportError('Extension already loaded') - if not extension_class_entry: - raise ImportError('No extension class') - - extinst = extension_class_entry() - setattr(self, namespace, extinst) - - # `load_extension` registry - self.__extensions[namespace] = { - 'inst': extinst, - 'extension_class_entry': extension_class_entry, - 'atom': atom, - 'rss': rss - } diff --git a/ext/__init__.py b/ext/__init__.py deleted file mode 100644 index 0e2b628..0000000 --- a/ext/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -""" - =========== - feedgen.ext - =========== -""" diff --git a/ext/base.py b/ext/base.py deleted file mode 100644 index 521139e..0000000 --- a/ext/base.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.base - ~~~~~~~~~~~~~~~~ - - Basic FeedGenerator extension 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_ns(self): - '''Returns a dict that will be used in the namespace map for the feed. - ''' - return dict() - - def extend_rss(self, feed): - '''Extend a 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): - '''Extend an ATOM feed xml structure containing all previously set - fields. - - :param feed: The feed xml root element. - :returns: The feed root element. - ''' - return feed - - -class BaseEntryExtension(BaseExtension): - '''Basic FeedEntry extension. - ''' diff --git a/ext/dc.py b/ext/dc.py deleted file mode 100644 index f731c0b..0000000 --- a/ext/dc.py +++ /dev/null @@ -1,407 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.dc - ~~~~~~~~~~~~~~~~~~~ - - Extends the FeedGenerator to add Dubline Core Elements to the feeds. - - Descriptions partly taken from - http://dublincore.org/documents/dcmi-terms/#elements-coverage - - :copyright: 2013-2017, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -from feedgen.ext.base import BaseExtension -from feedgen.util import xml_elem - - -class DcBaseExtension(BaseExtension): - '''Dublin Core Elements extension for podcasts. - ''' - - def __init__(self): - # http://dublincore.org/documents/usageguide/elements.shtml - # http://dublincore.org/documents/dces/ - # http://dublincore.org/documents/dcmi-terms/ - self._dcelem_contributor = None - self._dcelem_coverage = None - self._dcelem_creator = None - self._dcelem_date = None - self._dcelem_description = None - self._dcelem_format = None - self._dcelem_identifier = None - self._dcelem_language = None - self._dcelem_publisher = None - self._dcelem_relation = None - self._dcelem_rights = None - self._dcelem_source = None - self._dcelem_subject = None - self._dcelem_title = None - self._dcelem_type = None - - def extend_ns(self): - return {'dc': 'http://purl.org/dc/elements/1.1/'} - - def _extend_xml(self, xml_element): - '''Extend xml_element with set DC fields. - - :param xml_element: etree element - ''' - DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/' - - for elem in ['contributor', 'coverage', 'creator', 'date', - 'description', 'language', 'publisher', 'relation', - 'rights', 'source', 'subject', 'title', 'type', 'format', - 'identifier']: - if hasattr(self, '_dcelem_%s' % elem): - for val in getattr(self, '_dcelem_%s' % elem) or []: - node = xml_elem('{%s}%s' % (DCELEMENTS_NS, elem), - xml_element) - node.text = val - - def extend_atom(self, atom_feed): - '''Extend an Atom feed with the set DC fields. - - :param atom_feed: The feed root element - :returns: The feed root element - ''' - - self._extend_xml(atom_feed) - - return atom_feed - - def extend_rss(self, rss_feed): - '''Extend a RSS feed with the set DC fields. - - :param rss_feed: The feed root element - :returns: The feed root element. - ''' - channel = rss_feed[0] - self._extend_xml(channel) - - return rss_feed - - def dc_contributor(self, contributor=None, replace=False): - '''Get or set the dc:contributor which is an entity responsible for - making contributions to the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-contributor - - :param contributor: Contributor or list of contributors. - :param replace: Replace alredy set contributors (deault: False). - :returns: List of contributors. - ''' - if contributor is not None: - if not isinstance(contributor, list): - contributor = [contributor] - if replace or not self._dcelem_contributor: - self._dcelem_contributor = [] - self._dcelem_contributor += contributor - return self._dcelem_contributor - - def dc_coverage(self, coverage=None, replace=True): - '''Get or set the dc:coverage which indicated the spatial or temporal - topic of the resource, the spatial applicability of the resource, or - the jurisdiction under which the resource is relevant. - - Spatial topic and spatial applicability may be a named place or a - location specified by its geographic coordinates. Temporal topic may be - a named period, date, or date range. A jurisdiction may be a named - administrative entity or a geographic place to which the resource - applies. Recommended best practice is to use a controlled vocabulary - such as the Thesaurus of Geographic Names [TGN]. Where appropriate, - named places or time periods can be used in preference to numeric - identifiers such as sets of coordinates or date ranges. - - References: - [TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html - - :param coverage: Coverage of the feed. - :param replace: Replace already set coverage (default: True). - :returns: Coverage of the feed. - ''' - if coverage is not None: - if not isinstance(coverage, list): - coverage = [coverage] - if replace or not self._dcelem_coverage: - self._dcelem_coverage = [] - self._dcelem_coverage = coverage - return self._dcelem_coverage - - def dc_creator(self, creator=None, replace=False): - '''Get or set the dc:creator which is an entity primarily responsible - for making the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-creator - - :param creator: Creator or list of creators. - :param replace: Replace alredy set creators (deault: False). - :returns: List of creators. - ''' - if creator is not None: - if not isinstance(creator, list): - creator = [creator] - if replace or not self._dcelem_creator: - self._dcelem_creator = [] - self._dcelem_creator += creator - return self._dcelem_creator - - def dc_date(self, date=None, replace=True): - '''Get or set the dc:date which describes a point or period of time - associated with an event in the lifecycle of the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-date - - :param date: Date or list of dates. - :param replace: Replace alredy set dates (deault: True). - :returns: List of dates. - ''' - if date is not None: - if not isinstance(date, list): - date = [date] - if replace or not self._dcelem_date: - self._dcelem_date = [] - self._dcelem_date += date - return self._dcelem_date - - def dc_description(self, description=None, replace=True): - '''Get or set the dc:description which is an account of the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-description - - :param description: Description or list of descriptions. - :param replace: Replace alredy set descriptions (deault: True). - :returns: List of descriptions. - ''' - if description is not None: - if not isinstance(description, list): - description = [description] - if replace or not self._dcelem_description: - self._dcelem_description = [] - self._dcelem_description += description - return self._dcelem_description - - def dc_format(self, format=None, replace=True): - '''Get or set the dc:format which describes the file format, physical - medium, or dimensions of the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-format - - :param format: Format of the resource or list of formats. - :param replace: Replace alredy set format (deault: True). - :returns: Format of the resource. - ''' - if format is not None: - if not isinstance(format, list): - format = [format] - if replace or not self._dcelem_format: - self._dcelem_format = [] - self._dcelem_format += format - return self._dcelem_format - - def dc_identifier(self, identifier=None, replace=True): - '''Get or set the dc:identifier which should be an unambiguous - reference to the resource within a given context. - - For more inidentifierion see: - http://dublincore.org/documents/dcmi-terms/#elements-identifier - - :param identifier: Identifier of the resource or list of identifiers. - :param replace: Replace alredy set identifier (deault: True). - :returns: Identifiers of the resource. - ''' - if identifier is not None: - if not isinstance(identifier, list): - identifier = [identifier] - if replace or not self._dcelem_identifier: - self._dcelem_identifier = [] - self._dcelem_identifier += identifier - return self._dcelem_identifier - - def dc_language(self, language=None, replace=True): - '''Get or set the dc:language which describes a language of the - resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-language - - :param language: Language or list of languages. - :param replace: Replace alredy set languages (deault: True). - :returns: List of languages. - ''' - if language is not None: - if not isinstance(language, list): - language = [language] - if replace or not self._dcelem_language: - self._dcelem_language = [] - self._dcelem_language += language - return self._dcelem_language - - def dc_publisher(self, publisher=None, replace=False): - '''Get or set the dc:publisher which is an entity responsible for - making the resource available. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-publisher - - :param publisher: Publisher or list of publishers. - :param replace: Replace alredy set publishers (deault: False). - :returns: List of publishers. - ''' - if publisher is not None: - if not isinstance(publisher, list): - publisher = [publisher] - if replace or not self._dcelem_publisher: - self._dcelem_publisher = [] - self._dcelem_publisher += publisher - return self._dcelem_publisher - - def dc_relation(self, relation=None, replace=False): - '''Get or set the dc:relation which describes a related resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-relation - - :param relation: Relation or list of relations. - :param replace: Replace alredy set relations (deault: False). - :returns: List of relations. - ''' - if relation is not None: - if not isinstance(relation, list): - relation = [relation] - if replace or not self._dcelem_relation: - self._dcelem_relation = [] - self._dcelem_relation += relation - return self._dcelem_relation - - def dc_rights(self, rights=None, replace=False): - '''Get or set the dc:rights which may contain information about rights - held in and over the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-rights - - :param rights: Rights information or list of rights information. - :param replace: Replace alredy set rightss (deault: False). - :returns: List of rights information. - ''' - if rights is not None: - if not isinstance(rights, list): - rights = [rights] - if replace or not self._dcelem_rights: - self._dcelem_rights = [] - self._dcelem_rights += rights - return self._dcelem_rights - - def dc_source(self, source=None, replace=False): - '''Get or set the dc:source which is a related resource from which the - described resource is derived. - - The described resource may be derived from the related resource in - whole or in part. Recommended best practice is to identify the related - resource by means of a string conforming to a formal identification - system. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-source - - :param source: Source or list of sources. - :param replace: Replace alredy set sources (deault: False). - :returns: List of sources. - ''' - if source is not None: - if not isinstance(source, list): - source = [source] - if replace or not self._dcelem_source: - self._dcelem_source = [] - self._dcelem_source += source - return self._dcelem_source - - def dc_subject(self, subject=None, replace=False): - '''Get or set the dc:subject which describes the topic of the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-subject - - :param subject: Subject or list of subjects. - :param replace: Replace alredy set subjects (deault: False). - :returns: List of subjects. - ''' - if subject is not None: - if not isinstance(subject, list): - subject = [subject] - if replace or not self._dcelem_subject: - self._dcelem_subject = [] - self._dcelem_subject += subject - return self._dcelem_subject - - def dc_title(self, title=None, replace=True): - '''Get or set the dc:title which is a name given to the resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-title - - :param title: Title or list of titles. - :param replace: Replace alredy set titles (deault: False). - :returns: List of titles. - ''' - if title is not None: - if not isinstance(title, list): - title = [title] - if replace or not self._dcelem_title: - self._dcelem_title = [] - self._dcelem_title += title - return self._dcelem_title - - def dc_type(self, type=None, replace=False): - '''Get or set the dc:type which describes the nature or genre of the - resource. - - For more information see: - http://dublincore.org/documents/dcmi-terms/#elements-type - - :param type: Type or list of types. - :param replace: Replace alredy set types (deault: False). - :returns: List of types. - ''' - if type is not None: - if not isinstance(type, list): - type = [type] - if replace or not self._dcelem_type: - self._dcelem_type = [] - self._dcelem_type += type - return self._dcelem_type - - -class DcExtension(DcBaseExtension): - '''Dublin Core Elements extension for podcasts. - ''' - - -class DcEntryExtension(DcBaseExtension): - '''Dublin Core Elements extension for podcasts. - ''' - def extend_atom(self, entry): - '''Add dc elements to an atom item. Alters the item itself. - - :param entry: An atom entry element. - :returns: The entry element. - ''' - self._extend_xml(entry) - return entry - - def extend_rss(self, item): - '''Add dc elements to a RSS item. Alters the item itself. - - :param item: A RSS item element. - :returns: The item element. - ''' - self._extend_xml(item) - return item diff --git a/ext/geo.py b/ext/geo.py deleted file mode 100644 index b6384d4..0000000 --- a/ext/geo.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.geo - ~~~~~~~~~~~~~~~~~~~ - - Extends the FeedGenerator to produce Simple GeoRSS feeds. - - :copyright: 2017, Bob Breznak - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -from feedgen.ext.base import BaseExtension - - -class GeoExtension(BaseExtension): - '''FeedGenerator extension for Simple GeoRSS. - ''' - - def extend_ns(self): - return {'georss': 'http://www.georss.org/georss'} diff --git a/ext/geo_entry.py b/ext/geo_entry.py deleted file mode 100644 index bb06cc2..0000000 --- a/ext/geo_entry.py +++ /dev/null @@ -1,329 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.geo_entry - ~~~~~~~~~~~~~~~~~~~ - - Extends the FeedGenerator to produce Simple GeoRSS feeds. - - :copyright: 2017, Bob Breznak - - :license: FreeBSD and LGPL, see license.* for more details. -''' -import numbers -import warnings - -from feedgen.ext.base import BaseEntryExtension -from feedgen.util import xml_elem - - -class GeoRSSPolygonInteriorWarning(Warning): - """ - Simple placeholder for warning about ignored polygon interiors. - - Stores the original geom on a ``geom`` attribute (if required warnings are - raised as errors). - """ - - def __init__(self, geom, *args, **kwargs): - self.geom = geom - super(GeoRSSPolygonInteriorWarning, self).__init__(*args, **kwargs) - - def __str__(self): - return '{:d} interiors of polygon ignored'.format( - len(self.geom.__geo_interface__['coordinates']) - 1 - ) # ignore exterior in count - - -class GeoRSSGeometryError(ValueError): - """ - Subclass of ValueError for a GeoRSS geometry error - - Only some geometries are supported in Simple GeoRSS, so if not raise an - error. Offending geometry is stored on the ``geom`` attribute. - """ - - def __init__(self, geom, *args, **kwargs): - self.geom = geom - super(GeoRSSGeometryError, self).__init__(*args, **kwargs) - - def __str__(self): - msg = "Geometry of type '{}' not in Point, Linestring or Polygon" - return msg.format( - self.geom.__geo_interface__['type'] - ) - - -class GeoEntryExtension(BaseEntryExtension): - '''FeedEntry extension for Simple GeoRSS. - ''' - - def __init__(self): - '''Simple GeoRSS tag''' - # geometries - self.__point = None - self.__line = None - self.__polygon = None - self.__box = None - - # additional properties - self.__featuretypetag = None - self.__relationshiptag = None - self.__featurename = None - - # elevation - self.__elev = None - self.__floor = None - - # radius - self.__radius = None - - def extend_file(self, entry): - '''Add additional fields to an RSS item. - - :param feed: The RSS item XML element to use. - ''' - - GEO_NS = 'http://www.georss.org/georss' - - if self.__point: - point = xml_elem('{%s}point' % GEO_NS, entry) - point.text = self.__point - - if self.__line: - line = xml_elem('{%s}line' % GEO_NS, entry) - line.text = self.__line - - if self.__polygon: - polygon = xml_elem('{%s}polygon' % GEO_NS, entry) - polygon.text = self.__polygon - - if self.__box: - box = xml_elem('{%s}box' % GEO_NS, entry) - box.text = self.__box - - if self.__featuretypetag: - featuretypetag = xml_elem('{%s}featuretypetag' % GEO_NS, entry) - featuretypetag.text = self.__featuretypetag - - if self.__relationshiptag: - relationshiptag = xml_elem('{%s}relationshiptag' % GEO_NS, entry) - relationshiptag.text = self.__relationshiptag - - if self.__featurename: - featurename = xml_elem('{%s}featurename' % GEO_NS, entry) - featurename.text = self.__featurename - - if self.__elev: - elevation = xml_elem('{%s}elev' % GEO_NS, entry) - elevation.text = str(self.__elev) - - if self.__floor: - floor = xml_elem('{%s}floor' % GEO_NS, entry) - floor.text = str(self.__floor) - - if self.__radius: - radius = xml_elem('{%s}radius' % GEO_NS, entry) - radius.text = str(self.__radius) - - return entry - - def extend_rss(self, entry): - return self.extend_file(entry) - - def extend_atom(self, entry): - return self.extend_file(entry) - - def point(self, point=None): - '''Get or set the georss:point of the entry. - - :param point: The GeoRSS formatted point (i.e. "42.36 -71.05") - :returns: The current georss:point of the entry. - ''' - - if point is not None: - self.__point = point - - return self.__point - - def line(self, line=None): - '''Get or set the georss:line of the entry - - :param point: The GeoRSS formatted line (i.e. "45.256 -110.45 46.46 - -109.48 43.84 -109.86") - :return: The current georss:line of the entry - ''' - if line is not None: - self.__line = line - - return self.__line - - def polygon(self, polygon=None): - '''Get or set the georss:polygon of the entry - - :param polygon: The GeoRSS formatted polygon (i.e. "45.256 -110.45 - 46.46 -109.48 43.84 -109.86 45.256 -110.45") - :return: The current georss:polygon of the entry - ''' - if polygon is not None: - self.__polygon = polygon - - return self.__polygon - - def box(self, box=None): - ''' - Get or set the georss:box of the entry - - :param box: The GeoRSS formatted box (i.e. "42.943 -71.032 43.039 - -69.856") - :return: The current georss:box of the entry - ''' - if box is not None: - self.__box = box - - return self.__box - - def featuretypetag(self, featuretypetag=None): - ''' - Get or set the georss:featuretypetag of the entry - - :param featuretypetag: The GeoRSS feaaturertyptag (e.g. "city") - :return: The current georss:featurertypetag - ''' - if featuretypetag is not None: - self.__featuretypetag = featuretypetag - - return self.__featuretypetag - - def relationshiptag(self, relationshiptag=None): - ''' - Get or set the georss:relationshiptag of the entry - - :param relationshiptag: The GeoRSS relationshiptag (e.g. - "is-centred-at") - :return: the current georss:relationshiptag - ''' - if relationshiptag is not None: - self.__relationshiptag = relationshiptag - - return self.__relationshiptag - - def featurename(self, featurename=None): - ''' - Get or set the georss:featurename of the entry - - :param featuretypetag: The GeoRSS featurename (e.g. "Footscray") - :return: the current georss:featurename - ''' - if featurename is not None: - self.__featurename = featurename - - return self.__featurename - - def elev(self, elev=None): - ''' - Get or set the georss:elev of the entry - - :param elev: The GeoRSS elevation (e.g. 100.3) - :type elev: numbers.Number - :return: the current georss:elev - ''' - if elev is not None: - if not isinstance(elev, numbers.Number): - raise ValueError("elev tag must be numeric: {}".format(elev)) - - self.__elev = elev - - return self.__elev - - def floor(self, floor=None): - ''' - Get or set the georss:floor of the entry - - :param floor: The GeoRSS floor (e.g. 4) - :type floor: int - :return: the current georss:floor - ''' - if floor is not None: - if not isinstance(floor, int): - raise ValueError("floor tag must be int: {}".format(floor)) - - self.__floor = floor - - return self.__floor - - def radius(self, radius=None): - ''' - Get or set the georss:radius of the entry - - :param radius: The GeoRSS radius (e.g. 100.3) - :type radius: numbers.Number - :return: the current georss:radius - ''' - if radius is not None: - if not isinstance(radius, numbers.Number): - raise ValueError( - "radius tag must be numeric: {}".format(radius) - ) - - self.__radius = radius - - return self.__radius - - def geom_from_geo_interface(self, geom): - ''' - Generate a georss geometry from some Python object with a - ``__geo_interface__`` property (see the `geo_interface specification by - Sean Gillies`_geointerface ) - - Note only a subset of GeoJSON (see `geojson.org`_geojson ) can be - easily converted to GeoRSS: - - - Point - - LineString - - Polygon (if there are holes / donuts in the polygons a warning will - be generaated - - Other GeoJson types will raise a ``ValueError``. - - .. note:: The geometry is assumed to be x, y as longitude, latitude in - the WGS84 projection. - - .. _geointerface: https://gist.github.com/sgillies/2217756 - .. _geojson: https://geojson.org/ - - :param geom: Geometry object with a __geo_interface__ property - :return: the formatted GeoRSS geometry - ''' - geojson = geom.__geo_interface__ - - if geojson['type'] not in ('Point', 'LineString', 'Polygon'): - raise GeoRSSGeometryError(geom) - - if geojson['type'] == 'Point': - - coords = '{:f} {:f}'.format( - geojson['coordinates'][1], # latitude is y - geojson['coordinates'][0] - ) - return self.point(coords) - - elif geojson['type'] == 'LineString': - - coords = ' '.join( - '{:f} {:f}'.format(vertex[1], vertex[0]) - for vertex in - geojson['coordinates'] - ) - return self.line(coords) - - elif geojson['type'] == 'Polygon': - - if len(geojson['coordinates']) > 1: - warnings.warn(GeoRSSPolygonInteriorWarning(geom)) - - coords = ' '.join( - '{:f} {:f}'.format(vertex[1], vertex[0]) - for vertex in - geojson['coordinates'][0] - ) - return self.polygon(coords) diff --git a/ext/media.py b/ext/media.py deleted file mode 100644 index 74a5317..0000000 --- a/ext/media.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.media - ~~~~~~~~~~~~~~~~~ - - Extends the feedgen to produce media tags. - - :copyright: 2013-2017, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -from feedgen.ext.base import BaseEntryExtension, BaseExtension -from feedgen.util import ensure_format, xml_elem - -MEDIA_NS = 'http://search.yahoo.com/mrss/' - - -class MediaExtension(BaseExtension): - '''FeedGenerator extension for torrent feeds. - ''' - - def extend_ns(self): - return {'media': MEDIA_NS} - - -class MediaEntryExtension(BaseEntryExtension): - '''FeedEntry extension for media tags. - ''' - - def __init__(self): - self.__media_content = [] - self.__media_thumbnail = [] - - def extend_atom(self, entry): - '''Add additional fields to an RSS item. - - :param feed: The RSS item XML element to use. - ''' - - groups = {None: entry} - for media_content in self.__media_content: - # Define current media:group - group = groups.get(media_content.get('group')) - if group is None: - group = xml_elem('{%s}group' % MEDIA_NS, entry) - groups[media_content.get('group')] = group - # Add content - content = xml_elem('{%s}content' % MEDIA_NS, group) - for attr in ('url', 'fileSize', 'type', 'medium', 'isDefault', - 'expression', 'bitrate', 'framerate', 'samplingrate', - 'channels', 'duration', 'height', 'width', 'lang'): - if media_content.get(attr): - content.set(attr, media_content[attr]) - - for media_thumbnail in self.__media_thumbnail: - # Define current media:group - group = groups.get(media_thumbnail.get('group')) - if group is None: - group = xml_elem('{%s}group' % MEDIA_NS, entry) - groups[media_thumbnail.get('group')] = group - # Add thumbnails - thumbnail = xml_elem('{%s}thumbnail' % MEDIA_NS, group) - for attr in ('url', 'height', 'width', 'time'): - if media_thumbnail.get(attr): - thumbnail.set(attr, media_thumbnail[attr]) - - return entry - - def extend_rss(self, item): - return self.extend_atom(item) - - def content(self, content=None, replace=False, group='default', **kwargs): - '''Get or set media:content data. - - This method can be called with: - - the fields of a media:content as keyword arguments - - the fields of a media:content as a dictionary - - a list of dictionaries containing the media:content fields - - is a sub-element of either or . - Media objects that are not the same content should not be included in - the same element. The sequence of these items implies - the order of presentation. While many of the attributes appear to be - audio/video specific, this element can be used to publish any type - of media. It contains 14 attributes, most of which are optional. - - media:content has the following fields: - - *url* should specify the direct URL to the media object. - - *fileSize* number of bytes of the media object. - - *type* standard MIME type of the object. - - *medium* type of object (image | audio | video | document | - executable). - - *isDefault* determines if this is the default object. - - *expression* determines if the object is a sample or the full version - of the object, or even if it is a continuous stream (sample | full | - nonstop). - - *bitrate* kilobits per second rate of media. - - *framerate* number of frames per second for the media object. - - *samplingrate* number of samples per second taken to create the media - object. It is expressed in thousands of samples per second (kHz). - - *channels* number of audio channels in the media object. - - *duration* number of seconds the media object plays. - - *height* height of the media object. - - *width* width of the media object. - - *lang* is the primary language encapsulated in the media object. - - :param content: Dictionary or list of dictionaries with content data. - :param replace: Add or replace old data. - :param group: Media group to put this content in. - - :returns: The media content tag. - ''' - # Handle kwargs - if content is None and kwargs: - content = kwargs - # Handle new data - if content is not None: - # Reset data if we want to replace them - if replace or self.__media_content is None: - self.__media_content = [] - # Ensure list - if not isinstance(content, list): - content = [content] - # define media group - for c in content: - c['group'] = c.get('group', group) - self.__media_content += ensure_format( - content, - set(['url', 'fileSize', 'type', 'medium', 'isDefault', - 'expression', 'bitrate', 'framerate', 'samplingrate', - 'channels', 'duration', 'height', 'width', 'lang', - 'group']), - set(['url', 'group'])) - return self.__media_content - - def thumbnail(self, thumbnail=None, replace=False, group='default', - **kwargs): - '''Get or set media:thumbnail data. - - This method can be called with: - - the fields of a media:content as keyword arguments - - the fields of a media:content as a dictionary - - a list of dictionaries containing the media:content fields - - Allows particular images to be used as representative images for - the media object. If multiple thumbnails are included, and time - coding is not at play, it is assumed that the images are in order - of importance. It has one required attribute and three optional - attributes. - - media:thumbnail has the following fields: - - *url* should specify the direct URL to the media object. - - *height* height of the media object. - - *width* width of the media object. - - *time* specifies the time offset in relation to the media object. - - :param thumbnail: Dictionary or list of dictionaries with thumbnail - data. - :param replace: Add or replace old data. - :param group: Media group to put this content in. - - :returns: The media thumbnail tag. - ''' - # Handle kwargs - if thumbnail is None and kwargs: - thumbnail = kwargs - # Handle new data - if thumbnail is not None: - # Reset data if we want to replace them - if replace or self.__media_thumbnail is None: - self.__media_thumbnail = [] - # Ensure list - if not isinstance(thumbnail, list): - thumbnail = [thumbnail] - # Define media group - for t in thumbnail: - t['group'] = t.get('group', group) - self.__media_thumbnail += ensure_format( - thumbnail, - set(['url', 'height', 'width', 'time', 'group']), - set(['url', 'group'])) - return self.__media_thumbnail diff --git a/ext/podcast.py b/ext/podcast.py deleted file mode 100644 index 4c7eb0b..0000000 --- a/ext/podcast.py +++ /dev/null @@ -1,355 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.podcast - ~~~~~~~~~~~~~~~~~~~ - - Extends the FeedGenerator to produce podcasts. - - :copyright: 2013, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -from feedgen.compat import string_types -from feedgen.ext.base import BaseExtension -from feedgen.util import ensure_format, xml_elem - - -class PodcastExtension(BaseExtension): - '''FeedGenerator extension for podcasts. - ''' - - def __init__(self): - # ITunes tags - # http://www.apple.com/itunes/podcasts/specs.html#rss - self.__itunes_author = None - self.__itunes_block = None - self.__itunes_category = None - self.__itunes_image = None - self.__itunes_explicit = None - self.__itunes_complete = None - self.__itunes_new_feed_url = None - self.__itunes_owner = None - self.__itunes_subtitle = 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): - '''Extend an RSS feed root with set itunes fields. - - :returns: The feed root element. - ''' - ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' - channel = rss_feed[0] - - if self.__itunes_author: - author = xml_elem('{%s}author' % ITUNES_NS, channel) - author.text = self.__itunes_author - - if self.__itunes_block is not None: - block = xml_elem('{%s}block' % ITUNES_NS, channel) - block.text = 'yes' if self.__itunes_block else 'no' - - for c in self.__itunes_category or []: - if not c.get('cat'): - continue - category = channel.find( - '{%s}category[@text="%s"]' % (ITUNES_NS, c.get('cat'))) - if category is None: - category = xml_elem('{%s}category' % ITUNES_NS, channel) - category.attrib['text'] = c.get('cat') - - if c.get('sub'): - subcategory = xml_elem('{%s}category' % ITUNES_NS, category) - subcategory.attrib['text'] = c.get('sub') - - if self.__itunes_image: - image = xml_elem('{%s}image' % ITUNES_NS, channel) - image.attrib['href'] = self.__itunes_image - - if self.__itunes_explicit in ('yes', 'no', 'clean'): - explicit = xml_elem('{%s}explicit' % ITUNES_NS, channel) - explicit.text = self.__itunes_explicit - - if self.__itunes_complete in ('yes', 'no'): - complete = xml_elem('{%s}complete' % ITUNES_NS, channel) - complete.text = self.__itunes_complete - - if self.__itunes_new_feed_url: - new_feed_url = xml_elem('{%s}new-feed-url' % ITUNES_NS, channel) - new_feed_url.text = self.__itunes_new_feed_url - - if self.__itunes_owner: - owner = xml_elem('{%s}owner' % ITUNES_NS, channel) - owner_name = xml_elem('{%s}name' % ITUNES_NS, owner) - owner_name.text = self.__itunes_owner.get('name') - owner_email = xml_elem('{%s}email' % ITUNES_NS, owner) - owner_email.text = self.__itunes_owner.get('email') - - if self.__itunes_subtitle: - subtitle = xml_elem('{%s}subtitle' % ITUNES_NS, channel) - subtitle.text = self.__itunes_subtitle - - if self.__itunes_summary: - summary = xml_elem('{%s}summary' % ITUNES_NS, channel) - summary.text = self.__itunes_summary - - return rss_feed - - def itunes_author(self, itunes_author=None): - '''Get or set the itunes:author. The content of this tag is shown in - the Artist column in iTunes. If the tag is not present, iTunes uses the - contents of the tag. If is not present at the - feed level, iTunes will use the contents of . - - :param itunes_author: The author of the podcast. - :returns: The author of the podcast. - ''' - if itunes_author is not None: - self.__itunes_author = itunes_author - return self.__itunes_author - - def itunes_block(self, itunes_block=None): - '''Get or set the ITunes block attribute. Use this to prevent the - entire podcast from appearing in the iTunes podcast directory. - - :param itunes_block: Block the podcast. - :returns: If the podcast is blocked. - ''' - if itunes_block is not None: - self.__itunes_block = itunes_block - return self.__itunes_block - - def itunes_category(self, itunes_category=None, replace=False, **kwargs): - '''Get or set the ITunes category which appears in the category column - and in iTunes Store Browser. - - The (sub-)category has to be one from the values defined at - http://www.apple.com/itunes/podcasts/specs.html#categories - - This method can be called with: - - - the fields of an itunes_category as keyword arguments - - the fields of an itunes_category as a dictionary - - a list of dictionaries containing the itunes_category fields - - An itunes_category has the following fields: - - - *cat* name for a category. - - *sub* name for a subcategory, child of category - - If a podcast has more than one subcategory from the same category, the - category is called more than once. - - Likei the parameter:: - - [{"cat":"Arts","sub":"Design"},{"cat":"Arts","sub":"Food"}] - - …would become:: - - - - - - - - :param itunes_category: Dictionary or list of dictionaries with - itunes_category data. - :param replace: Add or replace old data. - :returns: List of itunes_categories as dictionaries. - - --- - - **Important note about deprecated parameter syntax:** Old version of - the feedgen did only support one category plus one subcategory which - would be passed to this ducntion as first two parameters. For - compatibility reasons, this still works but should not be used any may - be removed at any time. - ''' - # Ensure old API still works for now. Note that the API is deprecated - # and this fallback may be removed at any time. - if isinstance(itunes_category, string_types): - itunes_category = {'cat': itunes_category} - if replace: - itunes_category['sub'] = replace - replace = True - if itunes_category is None and kwargs: - itunes_category = kwargs - if itunes_category is not None: - if replace or self.__itunes_category is None: - self.__itunes_category = [] - self.__itunes_category += ensure_format(itunes_category, - set(['cat', 'sub']), - set(['cat'])) - return self.__itunes_category - - def itunes_image(self, itunes_image=None): - '''Get or set the image for the podcast. This tag specifies the artwork - for your podcast. Put the URL to the image in the href attribute. - iTunes prefers square .jpg images that are at least 1400x1400 pixels, - which is different from what is specified for the standard RSS image - tag. In order for a podcast to be eligible for an iTunes Store feature, - the accompanying image must be at least 1400x1400 pixels. - - iTunes supports images in JPEG and PNG formats with an RGB color space - (CMYK is not supported). The URL must end in ".jpg" or ".png". If the - tag is not present, iTunes will use the contents of the - RSS image tag. - - If you change your podcast’s image, also change the file’s name. iTunes - may not change the image if it checks your feed and the image URL is - the same. The server hosting your cover art image must allow HTTP head - requests for iTS to be able to automatically update your cover art. - - :param itunes_image: Image of the podcast. - :returns: Image of the podcast. - ''' - if itunes_image is not None: - if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'): - self.__itunes_image = itunes_image - else: - ValueError('Image file must be png or jpg') - return self.__itunes_image - - def itunes_explicit(self, itunes_explicit=None): - '''Get or the the itunes:explicit value of the podcast. This tag should - be used to indicate whether your podcast contains explicit material. - The three values for this tag are "yes", "no", and "clean". - - If you populate this tag with "yes", an "explicit" parental advisory - graphic will appear next to your podcast artwork on the iTunes Store - and in the Name column in iTunes. If the value is "clean", the parental - advisory type is considered Clean, meaning that no explicit language or - adult content is included anywhere in the episodes, and a "clean" - graphic will appear. If the explicit tag is present and has any other - value (e.g., "no"), you see no indicator — blank is the default - advisory type. - - :param itunes_explicit: If the podcast contains explicit material. - :returns: If the podcast contains explicit material. - ''' - if itunes_explicit is not None: - if itunes_explicit not in ('', 'yes', 'no', 'clean'): - raise ValueError('Invalid value for explicit tag') - self.__itunes_explicit = itunes_explicit - return self.__itunes_explicit - - def itunes_complete(self, itunes_complete=None): - '''Get or set the itunes:complete value of the podcast. This tag can be - used to indicate the completion of a podcast. - - If you populate this tag with "yes", you are indicating that no more - episodes will be added to the podcast. If the tag is - present and has any other value (e.g. “no”), it will have no effect on - the podcast. - - :param itunes_complete: If the podcast is complete. - :returns: If the podcast is complete. - ''' - if itunes_complete is not None: - if itunes_complete not in ('yes', 'no', '', True, False): - raise ValueError('Invalid value for complete tag') - if itunes_complete is True: - itunes_complete = 'yes' - if itunes_complete is False: - itunes_complete = 'no' - self.__itunes_complete = itunes_complete - return self.__itunes_complete - - def itunes_new_feed_url(self, itunes_new_feed_url=None): - '''Get or set the new-feed-url property of the podcast. This tag allows - you to change the URL where the podcast feed is located - - After adding the tag to your old feed, you should maintain the old feed - for 48 hours before retiring it. At that point, iTunes will have - updated the directory with the new feed URL. - - :param itunes_new_feed_url: New feed URL. - :returns: New feed URL. - ''' - if itunes_new_feed_url is not None: - self.__itunes_new_feed_url = itunes_new_feed_url - return self.__itunes_new_feed_url - - def itunes_owner(self, name=None, email=None): - '''Get or set the itunes:owner of the podcast. This tag contains - information that will be used to contact the owner of the podcast for - communication specifically about the podcast. It will not be publicly - displayed. - - :param itunes_owner: The owner of the feed. - :returns: Data of the owner of the feed. - ''' - if name is not None: - if name and email: - self.__itunes_owner = {'name': name, 'email': email} - elif not name and not email: - self.__itunes_owner = None - else: - raise ValueError('Both name and email have to be set.') - return self.__itunes_owner - - def itunes_subtitle(self, itunes_subtitle=None): - '''Get or set the itunes:subtitle value for the podcast. The contents of - this tag are shown in the Description column in iTunes. The subtitle - displays best if it is only a few words long. - - :param itunes_subtitle: Subtitle of the podcast. - :returns: Subtitle of the podcast. - ''' - if itunes_subtitle is not None: - self.__itunes_subtitle = itunes_subtitle - return self.__itunes_subtitle - - def itunes_summary(self, itunes_summary=None): - '''Get or set the itunes:summary value for the podcast. The contents of - this tag are shown in a separate window that appears when the "circled - i" in the Description column is clicked. It also appears on the iTunes - page for your podcast. This field can be up to 4000 characters. If - `` is not included, the contents of the - tag are used. - - :param itunes_summary: Summary of the podcast. - :returns: Summary of the podcast. - ''' - if itunes_summary is not None: - self.__itunes_summary = itunes_summary - return self.__itunes_summary - - _itunes_categories = { - 'Arts': [ - 'Design', 'Fashion & Beauty', 'Food', 'Literature', - 'Performing Arts', 'Visual Arts'], - 'Business': [ - 'Business News', 'Careers', 'Investing', - 'Management & Marketing', 'Shopping'], - 'Comedy': [], - 'Education': [ - 'Education', 'Education Technology', 'Higher Education', - 'K-12', 'Language Courses', 'Training'], - 'Games & Hobbies': [ - 'Automotive', 'Aviation', 'Hobbies', 'Other Games', - 'Video Games'], - 'Government & Organizations': [ - 'Local', 'National', 'Non-Profit', 'Regional'], - 'Health': [ - 'Alternative Health', 'Fitness & Nutrition', 'Self-Help', - 'Sexuality'], - 'Kids & Family': [], - 'Music': [], - 'News & Politics': [], - 'Religion & Spirituality': [ - 'Buddhism', 'Christianity', 'Hinduism', 'Islam', 'Judaism', - 'Other', 'Spirituality'], - 'Science & Medicine': [ - 'Medicine', 'Natural Sciences', 'Social Sciences'], - 'Society & Culture': [ - 'History', 'Personal Journals', 'Philosophy', - 'Places & Travel'], - 'Sports & Recreation': [ - 'Amateur', 'College & High School', 'Outdoor', 'Professional'], - 'Technology': [ - 'Gadgets', 'Tech News', 'Podcasting', 'Software How-To'], - 'TV & Film': []} diff --git a/ext/podcast_entry.py b/ext/podcast_entry.py deleted file mode 100644 index 2a3771f..0000000 --- a/ext/podcast_entry.py +++ /dev/null @@ -1,244 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.podcast_entry - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Extends the feedgen to produce podcasts. - - :copyright: 2013-2016, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -from feedgen.ext.base import BaseEntryExtension -from feedgen.util import xml_elem - - -class PodcastEntryExtension(BaseEntryExtension): - '''FeedEntry extension for podcasts. - ''' - - def __init__(self): - # ITunes tags - # http://www.apple.com/itunes/podcasts/specs.html#rss - self.__itunes_author = None - self.__itunes_block = None - self.__itunes_image = None - self.__itunes_duration = None - self.__itunes_explicit = None - self.__itunes_is_closed_captioned = None - self.__itunes_order = None - self.__itunes_subtitle = None - self.__itunes_summary = None - - def extend_rss(self, entry): - '''Add additional fields to an RSS item. - - :param feed: The RSS item XML element to use. - ''' - ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' - - if self.__itunes_author: - author = xml_elem('{%s}author' % ITUNES_NS, entry) - author.text = self.__itunes_author - - if self.__itunes_block is not None: - block = xml_elem('{%s}block' % ITUNES_NS, entry) - block.text = 'yes' if self.__itunes_block else 'no' - - if self.__itunes_image: - image = xml_elem('{%s}image' % ITUNES_NS, entry) - image.attrib['href'] = self.__itunes_image - - if self.__itunes_duration: - duration = xml_elem('{%s}duration' % ITUNES_NS, entry) - duration.text = self.__itunes_duration - - if self.__itunes_explicit in ('yes', 'no', 'clean'): - explicit = xml_elem('{%s}explicit' % ITUNES_NS, entry) - explicit.text = self.__itunes_explicit - - if self.__itunes_is_closed_captioned is not None: - is_closed_captioned = xml_elem( - '{%s}isClosedCaptioned' % ITUNES_NS, entry) - if self.__itunes_is_closed_captioned: - is_closed_captioned.text = 'yes' - else: - is_closed_captioned.text = 'no' - - if self.__itunes_order is not None and self.__itunes_order >= 0: - order = xml_elem('{%s}order' % ITUNES_NS, entry) - order.text = str(self.__itunes_order) - - if self.__itunes_subtitle: - subtitle = xml_elem('{%s}subtitle' % ITUNES_NS, entry) - subtitle.text = self.__itunes_subtitle - - if self.__itunes_summary: - summary = xml_elem('{%s}summary' % ITUNES_NS, entry) - summary.text = self.__itunes_summary - return entry - - def itunes_author(self, itunes_author=None): - '''Get or set the itunes:author of the podcast episode. The content of - this tag is shown in the Artist column in iTunes. If the tag is not - present, iTunes uses the contents of the tag. If - is not present at the feed level, iTunes will use the - contents of . - - :param itunes_author: The author of the podcast. - :returns: The author of the podcast. - ''' - if itunes_author is not None: - self.__itunes_author = itunes_author - return self.__itunes_author - - def itunes_block(self, itunes_block=None): - '''Get or set the ITunes block attribute. Use this to prevent episodes - from appearing in the iTunes podcast directory. - - :param itunes_block: Block podcast episodes. - :returns: If the podcast episode is blocked. - ''' - if itunes_block is not None: - self.__itunes_block = itunes_block - return self.__itunes_block - - def itunes_image(self, itunes_image=None): - '''Get or set the image for the podcast episode. This tag specifies the - artwork for your podcast. Put the URL to the image in the href - attribute. iTunes prefers square .jpg images that are at least - 1400x1400 pixels, which is different from what is specified for the - standard RSS image tag. In order for a podcast to be eligible for an - iTunes Store feature, the accompanying image must be at least 1400x1400 - pixels. - - iTunes supports images in JPEG and PNG formats with an RGB color space - (CMYK is not supported). The URL must end in ".jpg" or ".png". If the - tag is not present, iTunes will use the contents of the - RSS image tag. - - If you change your podcast’s image, also change the file’s name. iTunes - may not change the image if it checks your feed and the image URL is - the same. The server hosting your cover art image must allow HTTP head - requests for iTS to be able to automatically update your cover art. - - :param itunes_image: Image of the podcast. - :returns: Image of the podcast. - ''' - if itunes_image is not None: - if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'): - self.__itunes_image = itunes_image - else: - raise ValueError('Image file must be png or jpg') - return self.__itunes_image - - def itunes_duration(self, itunes_duration=None): - '''Get or set the duration of the podcast episode. The content of this - tag is shown in the Time column in iTunes. - - The tag can be formatted HH:MM:SS, H:MM:SS, MM:SS, or M:SS (H = hours, - M = minutes, S = seconds). If an integer is provided (no colon - present), the value is assumed to be in seconds. If one colon is - present, the number to the left is assumed to be minutes, and the - number to the right is assumed to be seconds. If more than two colons - are present, the numbers farthest to the right are ignored. - - :param itunes_duration: Duration of the podcast episode. - :returns: Duration of the podcast episode. - ''' - if itunes_duration is not None: - itunes_duration = str(itunes_duration) - if len(itunes_duration.split(':')) > 3 or \ - itunes_duration.lstrip('0123456789:') != '': - raise ValueError('Invalid duration format') - self.__itunes_duration = itunes_duration - return self.__itunes_duration - - def itunes_explicit(self, itunes_explicit=None): - '''Get or the the itunes:explicit value of the podcast episode. This - tag should be used to indicate whether your podcast episode contains - explicit material. The three values for this tag are "yes", "no", and - "clean". - - If you populate this tag with "yes", an "explicit" parental advisory - graphic will appear next to your podcast artwork on the iTunes Store - and in the Name column in iTunes. If the value is "clean", the parental - advisory type is considered Clean, meaning that no explicit language or - adult content is included anywhere in the episodes, and a "clean" - graphic will appear. If the explicit tag is present and has any other - value (e.g., "no"), you see no indicator — blank is the default - advisory type. - - :param itunes_explicit: If the podcast episode contains explicit - material. - :returns: If the podcast episode contains explicit material. - ''' - if itunes_explicit is not None: - if itunes_explicit not in ('', 'yes', 'no', 'clean'): - raise ValueError('Invalid value for explicit tag') - self.__itunes_explicit = itunes_explicit - return self.__itunes_explicit - - def itunes_is_closed_captioned(self, itunes_is_closed_captioned=None): - '''Get or set the is_closed_captioned value of the podcast episode. - This tag should be used if your podcast includes a video episode with - embedded closed captioning support. The two values for this tag are - "yes" and "no”. - - :param is_closed_captioned: If the episode has closed captioning - support. - :returns: If the episode has closed captioning support. - ''' - if itunes_is_closed_captioned is not None: - self.__itunes_is_closed_captioned = \ - itunes_is_closed_captioned in ('yes', True) - return self.__itunes_is_closed_captioned - - def itunes_order(self, itunes_order=None): - '''Get or set the itunes:order value of the podcast episode. This tag - can be used to override the default ordering of episodes on the store. - - This tag is used at an level by populating with the number value - in which you would like the episode to appear on the store. For - example, if you would like an to appear as the first episode in - the podcast, you would populate the tag with “1”. If - conflicting order values are present in multiple episodes, the store - will use default ordering (pubDate). - - To remove the order from the episode set the order to a value below - zero. - - :param itunes_order: The order of the episode. - :returns: The order of the episode. - ''' - if itunes_order is not None: - self.__itunes_order = int(itunes_order) - return self.__itunes_order - - def itunes_subtitle(self, itunes_subtitle=None): - '''Get or set the itunes:subtitle value for the podcast episode. The - contents of this tag are shown in the Description column in iTunes. The - subtitle displays best if it is only a few words long. - - :param itunes_subtitle: Subtitle of the podcast episode. - :returns: Subtitle of the podcast episode. - ''' - if itunes_subtitle is not None: - self.__itunes_subtitle = itunes_subtitle - return self.__itunes_subtitle - - def itunes_summary(self, itunes_summary=None): - '''Get or set the itunes:summary value for the podcast episode. The - contents of this tag are shown in a separate window that appears when - the "circled i" in the Description column is clicked. It also appears - on the iTunes page for your podcast. This field can be up to 4000 - characters. If is not included, the contents of the - tag are used. - - :param itunes_summary: Summary of the podcast episode. - :returns: Summary of the podcast episode. - ''' - if itunes_summary is not None: - self.__itunes_summary = itunes_summary - return self.__itunes_summary diff --git a/ext/syndication.py b/ext/syndication.py deleted file mode 100644 index 016b144..0000000 --- a/ext/syndication.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2015 Kenichi Sato -# - -''' -Extends FeedGenerator to support Syndication module - -See below for details -http://web.resource.org/rss/1.0/modules/syndication/ -''' - -from feedgen.ext.base import BaseExtension -from feedgen.util import xml_elem - -SYNDICATION_NS = 'http://purl.org/rss/1.0/modules/syndication/' -PERIOD_TYPE = ('hourly', 'daily', 'weekly', 'monthly', 'yearly') - - -def _set_value(channel, name, value): - if value: - newelem = xml_elem('{%s}' % SYNDICATION_NS + name, channel) - newelem.text = value - - -class SyndicationExtension(BaseExtension): - def __init__(self): - self._update_period = None - self._update_freq = None - self._update_base = None - - def extend_ns(self): - return {'sy': SYNDICATION_NS} - - def extend_rss(self, rss_feed): - channel = rss_feed[0] - _set_value(channel, 'UpdatePeriod', self._update_period) - _set_value(channel, 'UpdateFrequency', str(self._update_freq)) - _set_value(channel, 'UpdateBase', self._update_base) - - def update_period(self, value): - if value not in PERIOD_TYPE: - raise ValueError('Invalid update period value') - self._update_period = value - return self._update_period - - def update_frequency(self, value): - if type(value) is not int or value <= 0: - raise ValueError('Invalid update frequency value') - self._update_freq = value - return self._update_freq - - def update_base(self, value): - # the value should be in W3CDTF format - self._update_base = value - return self._update_base - - -class SyndicationEntryExtension(BaseExtension): - pass diff --git a/ext/torrent.py b/ext/torrent.py deleted file mode 100644 index 5548a81..0000000 --- a/ext/torrent.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.ext.torrent - ~~~~~~~~~~~~~~~~~~~ - - Extends the FeedGenerator to produce torrent feeds. - - :copyright: 2016, Raspbeguy - - :license: FreeBSD and LGPL, see license.* for more details. -''' - -from feedgen.ext.base import BaseEntryExtension, BaseExtension -from feedgen.util import xml_elem - -TORRENT_NS = 'http://xmlns.ezrss.it/0.1/dtd/' - - -class TorrentExtension(BaseExtension): - '''FeedGenerator extension for torrent feeds. - ''' - def extend_ns(self): - return {'torrent': TORRENT_NS} - - -class TorrentEntryExtension(BaseEntryExtension): - '''FeedEntry extension for torrent feeds - ''' - def __init__(self): - self.__torrent_filename = None - self.__torrent_infohash = None - self.__torrent_contentlength = None - self.__torrent_seeds = None - self.__torrent_peers = None - self.__torrent_verified = None - - def extend_rss(self, entry): - '''Add additional fields to an RSS item. - - :param feed: The RSS item XML element to use. - ''' - if self.__torrent_filename: - filename = xml_elem('{%s}filename' % TORRENT_NS, entry) - filename.text = self.__torrent_filename - - if self.__torrent_contentlength: - contentlength = xml_elem('{%s}contentlength' % TORRENT_NS, entry) - contentlength.text = self.__torrent_contentlength - - if self.__torrent_infohash: - infohash = xml_elem('{%s}infohash' % TORRENT_NS, entry) - infohash.text = self.__torrent_infohash - magnet = xml_elem('{%s}magneturi' % TORRENT_NS, entry) - magnet.text = 'magnet:?xt=urn:btih:' + self.__torrent_infohash - - if self.__torrent_seeds: - seeds = xml_elem('{%s}seed' % TORRENT_NS, entry) - seeds.text = self.__torrent_seeds - - if self.__torrent_peers: - peers = xml_elem('{%s}peers' % TORRENT_NS, entry) - peers.text = self.__torrent_peers - - if self.__torrent_verified: - verified = xml_elem('{%s}verified' % TORRENT_NS, entry) - verified.text = self.__torrent_verified - - def filename(self, torrent_filename=None): - '''Get or set the name of the torrent file. - - :param torrent_filename: The name of the torrent file. - :returns: The name of the torrent file. - ''' - if torrent_filename is not None: - self.__torrent_filename = torrent_filename - return self.__torrent_filename - - def infohash(self, torrent_infohash=None): - '''Get or set the hash of the target file. - - :param torrent_infohash: The target file hash. - :returns: The target hash file. - ''' - if torrent_infohash is not None: - self.__torrent_infohash = torrent_infohash - return self.__torrent_infohash - - def contentlength(self, torrent_contentlength=None): - '''Get or set the size of the target file. - - :param torrent_contentlength: The target file size. - :returns: The target file size. - ''' - if torrent_contentlength is not None: - self.__torrent_contentlength = torrent_contentlength - return self.__torrent_contentlength - - def seeds(self, torrent_seeds=None): - '''Get or set the number of seeds. - - :param torrent_seeds: The seeds number. - :returns: The seeds number. - ''' - if torrent_seeds is not None: - self.__torrent_seeds = torrent_seeds - return self.__torrent_seeds - - def peers(self, torrent_peers=None): - '''Get or set the number od peers - - :param torrent_infohash: The peers number. - :returns: The peers number. - ''' - if torrent_peers is not None: - self.__torrent_peers = torrent_peers - return self.__torrent_peers - - def verified(self, torrent_verified=None): - '''Get or set the number of verified peers. - - :param torrent_infohash: The verified peers number. - :returns: The verified peers number. - ''' - if torrent_verified is not None: - self.__torrent_verified = torrent_verified - return self.__torrent_verified diff --git a/feed.py b/feed.py deleted file mode 100644 index c6fccbb..0000000 --- a/feed.py +++ /dev/null @@ -1,1169 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.feed - ~~~~~~~~~~~~ - - :copyright: 2013-2020, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. - -''' - -import sys -from datetime import datetime - -import dateutil.parser -import dateutil.tz -from lxml import etree # nosec - not using this for parsing - -import feedgen.version -from feedgen.compat import string_types -from feedgen.entry import FeedEntry -from feedgen.util import ensure_format, formatRFC2822, xml_elem - -_feedgen_version = feedgen.version.version_str - - -class FeedGenerator(object): - '''FeedGenerator for generating ATOM and RSS feeds. - ''' - - def __init__(self): - self.__feed_entries = [] - - # ATOM - # https://tools.ietf.org/html/rfc4287 - # required - self.__atom_id = None - self.__atom_title = None - self.__atom_updated = datetime.now(dateutil.tz.tzutc()) - - # recommended - self.__atom_author = None # {name*, uri, email} - self.__atom_link = None # {href*, rel, type, hreflang, title, length} - - # optional - self.__atom_category = None # {term*, scheme, label} - self.__atom_contributor = None - self.__atom_generator = { - 'value': 'python-feedgen', - 'uri': 'https://lkiesow.github.io/python-feedgen', - 'version': feedgen.version.version_str} # {value*,uri,version} - self.__atom_icon = None - self.__atom_logo = None - self.__atom_rights = None - self.__atom_subtitle = None - - # other - self.__atom_feed_xml_lang = None - - # RSS - # http://www.rssboard.org/rss-specification - self.__rss_title = None - self.__rss_link = None - self.__rss_description = None - - self.__rss_category = None - self.__rss_cloud = None - self.__rss_copyright = None - self.__rss_docs = 'http://www.rssboard.org/rss-specification' - self.__rss_generator = 'python-feedgen' - self.__rss_image = None - self.__rss_language = None - self.__rss_lastBuildDate = datetime.now(dateutil.tz.tzutc()) - self.__rss_managingEditor = None - self.__rss_pubDate = None - self.__rss_rating = None - self.__rss_skipHours = None - self.__rss_skipDays = None - self.__rss_textInput = None - self.__rss_ttl = None - self.__rss_webMaster = None - - # Extension list: - self.__extensions = {} - - 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. - ''' - nsmap = dict() - if extensions: - for ext in self.__extensions.values() or []: - if ext.get('atom'): - nsmap.update(ext['inst'].extend_ns()) - - feed = xml_elem('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 - - if not (self.__atom_id and self.__atom_title and self.__atom_updated): - missing = ([] if self.__atom_title else ['title']) + \ - ([] if self.__atom_id else ['id']) + \ - ([] if self.__atom_updated else ['updated']) - missing = ', '.join(missing) - raise ValueError('Required fields not set (%s)' % missing) - id = xml_elem('id', feed) - id.text = self.__atom_id - title = xml_elem('title', feed) - title.text = self.__atom_title - updated = xml_elem('updated', feed) - updated.text = self.__atom_updated.isoformat() - - # Add author elements - for a in self.__atom_author or []: - # Atom requires a name. Skip elements without. - if not a.get('name'): - continue - author = xml_elem('author', feed) - for k in a.keys(): - e = xml_elem(k, author) - e.text = str(a.get(k)) - - for l in self.__atom_link or []: - link = xml_elem('link', feed, href=l['href']) - if l.get('rel'): - link.attrib['rel'] = l['rel'] - if l.get('type'): - link.attrib['type'] = l['type'] - if l.get('hreflang'): - link.attrib['hreflang'] = l['hreflang'] - if l.get('title'): - link.attrib['title'] = l['title'] - if l.get('length'): - link.attrib['length'] = l['length'] - - for c in self.__atom_category or []: - cat = xml_elem('category', feed, term=c['term']) - if c.get('scheme'): - cat.attrib['scheme'] = c['scheme'] - if c.get('label'): - cat.attrib['label'] = c['label'] - - # Add author elements - for c in self.__atom_contributor or []: - # Atom requires a name. Skip elements without. - if not c.get('name'): - continue - contrib = xml_elem('contributor', feed) - name = xml_elem('name', contrib) - name.text = c.get('name') - if c.get('email'): - email = xml_elem('email', contrib) - email.text = c.get('email') - if c.get('uri'): - uri = xml_elem('uri', contrib) - uri.text = c.get('uri') - - if self.__atom_generator and self.__atom_generator.get('value'): - generator = xml_elem('generator', feed) - generator.text = self.__atom_generator['value'] - if self.__atom_generator.get('uri'): - generator.attrib['uri'] = self.__atom_generator['uri'] - if self.__atom_generator.get('version'): - generator.attrib['version'] = self.__atom_generator['version'] - - if self.__atom_icon: - icon = xml_elem('icon', feed) - icon.text = self.__atom_icon - - if self.__atom_logo: - logo = xml_elem('logo', feed) - logo.text = self.__atom_logo - - if self.__atom_rights: - rights = xml_elem('rights', feed) - rights.text = self.__atom_rights - - if self.__atom_subtitle: - subtitle = xml_elem('subtitle', feed) - subtitle.text = self.__atom_subtitle - - if extensions: - for ext in self.__extensions.values() or []: - if ext.get('atom'): - ext['inst'].extend_atom(feed) - - for entry in self.__feed_entries: - entry = entry.atom_entry() - feed.append(entry) - - doc = etree.ElementTree(feed) - return feed, doc - - def atom_str(self, pretty=False, extensions=True, encoding='UTF-8', - xml_declaration=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). - :param encoding: Encoding used in the XML file (default: UTF-8). - :param xml_declaration: If an XML declaration should be added to the - output (Default: enabled). - :returns: String representation of the ATOM feed. - - **Return type:** The return type may vary between different Python - versions and your encoding parameters passed to this method. For - details have a look at the `lxml documentation - `_ - ''' - feed, doc = self._create_atom(extensions=extensions) - return etree.tostring(feed, pretty_print=pretty, encoding=encoding, - xml_declaration=xml_declaration) - - def atom_file(self, filename, extensions=True, pretty=False, - encoding='UTF-8', xml_declaration=True): - '''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). - :param pretty: If the feed should be split into multiple lines and - properly indented. - :param encoding: Encoding used in the XML file (default: UTF-8). - :param xml_declaration: If an XML declaration should be added to the - output (Default: enabled). - ''' - feed, doc = self._create_atom(extensions=extensions) - doc.write(filename, pretty_print=pretty, encoding=encoding, - xml_declaration=xml_declaration) - - 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. - ''' - 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 = xml_elem('rss', version='2.0', nsmap=nsmap) - channel = xml_elem('channel', feed) - if not (self.__rss_title and - self.__rss_link and - self.__rss_description): - missing = ([] if self.__rss_title else ['title']) + \ - ([] if self.__rss_link else ['link']) + \ - ([] if self.__rss_description else ['description']) - missing = ', '.join(missing) - raise ValueError('Required fields not set (%s)' % missing) - title = xml_elem('title', channel) - title.text = self.__rss_title - link = xml_elem('link', channel) - link.text = self.__rss_link - desc = xml_elem('description', channel) - desc.text = self.__rss_description - 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 = xml_elem('{http://www.w3.org/2005/Atom}link', - channel, href=ln['href'], rel='self') - if ln.get('type'): - selflink.attrib['type'] = ln['type'] - if ln.get('hreflang'): - selflink.attrib['hreflang'] = ln['hreflang'] - if ln.get('title'): - selflink.attrib['title'] = ln['title'] - if ln.get('length'): - selflink.attrib['length'] = ln['length'] - break - if self.__rss_category: - for cat in self.__rss_category: - category = xml_elem('category', channel) - category.text = cat['value'] - if cat.get('domain'): - category.attrib['domain'] = cat['domain'] - if self.__rss_cloud: - cloud = xml_elem('cloud', channel) - cloud.attrib['domain'] = self.__rss_cloud.get('domain') - cloud.attrib['port'] = self.__rss_cloud.get('port') - cloud.attrib['path'] = self.__rss_cloud.get('path') - cloud.attrib['registerProcedure'] = self.__rss_cloud.get( - 'registerProcedure') - cloud.attrib['protocol'] = self.__rss_cloud.get('protocol') - if self.__rss_copyright: - copyright = xml_elem('copyright', channel) - copyright.text = self.__rss_copyright - if self.__rss_docs: - docs = xml_elem('docs', channel) - docs.text = self.__rss_docs - if self.__rss_generator: - generator = xml_elem('generator', channel) - generator.text = self.__rss_generator - if self.__rss_image: - image = xml_elem('image', channel) - url = xml_elem('url', image) - url.text = self.__rss_image.get('url') - title = xml_elem('title', image) - title.text = self.__rss_image.get('title', self.__rss_title) - link = xml_elem('link', image) - link.text = self.__rss_image.get('link', self.__rss_link) - if self.__rss_image.get('width'): - width = xml_elem('width', image) - width.text = self.__rss_image.get('width') - if self.__rss_image.get('height'): - height = xml_elem('height', image) - height.text = self.__rss_image.get('height') - if self.__rss_image.get('description'): - description = xml_elem('description', image) - description.text = self.__rss_image.get('description') - if self.__rss_language: - language = xml_elem('language', channel) - language.text = self.__rss_language - if self.__rss_lastBuildDate: - lastBuildDate = xml_elem('lastBuildDate', channel) - - lastBuildDate.text = formatRFC2822(self.__rss_lastBuildDate) - if self.__rss_managingEditor: - managingEditor = xml_elem('managingEditor', channel) - managingEditor.text = self.__rss_managingEditor - if self.__rss_pubDate: - pubDate = xml_elem('pubDate', channel) - pubDate.text = formatRFC2822(self.__rss_pubDate) - if self.__rss_rating: - rating = xml_elem('rating', channel) - rating.text = self.__rss_rating - if self.__rss_skipHours: - skipHours = xml_elem('skipHours', channel) - for h in self.__rss_skipHours: - hour = xml_elem('hour', skipHours) - hour.text = str(h) - if self.__rss_skipDays: - skipDays = xml_elem('skipDays', channel) - for d in self.__rss_skipDays: - day = xml_elem('day', skipDays) - day.text = d - if self.__rss_textInput: - textInput = xml_elem('textInput', channel) - textInput.attrib['title'] = self.__rss_textInput.get('title') - textInput.attrib['description'] = \ - self.__rss_textInput.get('description') - textInput.attrib['name'] = self.__rss_textInput.get('name') - textInput.attrib['link'] = self.__rss_textInput.get('link') - if self.__rss_ttl: - ttl = xml_elem('ttl', channel) - ttl.text = str(self.__rss_ttl) - if self.__rss_webMaster: - webMaster = xml_elem('webMaster', channel) - webMaster.text = self.__rss_webMaster - - if extensions: - for ext in self.__extensions.values() or []: - if ext.get('rss'): - ext['inst'].extend_rss(feed) - - for entry in self.__feed_entries: - item = entry.rss_entry() - channel.append(item) - - doc = etree.ElementTree(feed) - return feed, doc - - def rss_str(self, pretty=False, extensions=True, encoding='UTF-8', - xml_declaration=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). - :param encoding: Encoding used in the XML file (default: UTF-8). - :param xml_declaration: If an XML declaration should be added to the - output (Default: enabled). - :returns: String representation of the RSS feed. - - **Return type:** The return type may vary between different Python - versions and your encoding parameters passed to this method. For - details have a look at the `lxml documentation - `_ - ''' - feed, doc = self._create_rss(extensions=extensions) - return etree.tostring(feed, pretty_print=pretty, encoding=encoding, - xml_declaration=xml_declaration) - - def rss_file(self, filename, extensions=True, pretty=False, - encoding='UTF-8', xml_declaration=True): - '''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). - :param pretty: If the feed should be split into multiple lines and - properly indented. - :param encoding: Encoding used in the XML file (default: UTF-8). - :param xml_declaration: If an XML declaration should be added to the - output (Default: enabled). - ''' - feed, doc = self._create_rss(extensions=extensions) - doc.write(filename, pretty_print=pretty, encoding=encoding, - xml_declaration=xml_declaration) - - 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 - associated website. Title is mandatory for both ATOM and RSS and should - not be blank. - - :param title: The new title of the feed. - :returns: The feeds title. - ''' - if title is not None: - self.__atom_title = title - self.__rss_title = title - return self.__atom_title - - def id(self, id=None): - '''Get or set the feed id which identifies the feed using a universally - unique and permanent URI. If you have a long-term, renewable lease on - your Internet domain name, then you can feel free to use your website's - address. This field is for ATOM only. It is mandatory for ATOM. - - :param id: New Id of the ATOM feed. - :returns: Id of the feed. - ''' - - if id is not None: - self.__atom_id = id - return self.__atom_id - - def updated(self, updated=None): - '''Set or get the updated value which indicates the last time the feed - was modified in a significant way. - - The value can either be a string which will automatically be parsed or - a datetime.datetime object. In any case it is necessary that the value - include timezone information. - - This will set both atom:updated and rss:lastBuildDate. - - Default value - If not set, updated has as value the current date and time. - - :param updated: The modification date. - :returns: Modification date as datetime.datetime - ''' - if updated is not None: - if isinstance(updated, string_types): - updated = dateutil.parser.parse(updated) - if not isinstance(updated, datetime): - raise ValueError('Invalid datetime format') - if updated.tzinfo is None: - raise ValueError('Datetime object has no timezone info') - self.__atom_updated = updated - self.__rss_lastBuildDate = updated - - return self.__atom_updated - - def lastBuildDate(self, lastBuildDate=None): - '''Set or get the lastBuildDate value which indicates the last time the - content of the channel changed. - - The value can either be a string which will automatically be parsed or - a datetime.datetime object. In any case it is necessary that the value - include timezone information. - - This will set both atom:updated and rss:lastBuildDate. - - Default value - If not set, lastBuildDate has as value the current date and time. - - :param lastBuildDate: The modification date. - :returns: Modification date as datetime.datetime - ''' - return self.updated(lastBuildDate) - - def author(self, author=None, replace=False, **kwargs): - '''Get or set author data. An author element is a dictionary containing - a name, an email address and a URI. Name is mandatory for ATOM, email - is mandatory for RSS. - - This method can be called with: - - - the fields of an author as keyword arguments - - the fields of an author as a dictionary - - a list of dictionaries containing the author fields - - An author has the following fields: - - - *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. - - Example:: - - >>> feedgen.author({'name':'John Doe', 'email':'jdoe@example.com'}) - [{'name':'John Doe','email':'jdoe@example.com'}] - - >>> feedgen.author([{'name':'Mr. X'},{'name':'Max'}]) - [{'name':'John Doe','email':'jdoe@example.com'}, - {'name':'John Doe'}, {'name':'Max'}] - - >>> feedgen.author(name='John Doe', email='jdoe@example.com', - replace=True) - [{'name':'John Doe','email':'jdoe@example.com'}] - - ''' - if author is None and kwargs: - author = kwargs - if author is not None: - if replace or self.__atom_author is None: - self.__atom_author = [] - self.__atom_author += [author] - self.__rss_author = [] - for a in self.__atom_author: - if a.get('email'): - self.__rss_author.append(a['email']) - return self.__atom_author - - def link(self, link=None, replace=False, **kwargs): - '''Get or set link data. An link element is a dict with the fields - href, rel, type, hreflang, title, and length. Href is mandatory for - ATOM. - - This method can be called with: - - - the fields of a link as keyword arguments - - the fields of a link as a dictionary - - a list of dictionaries containing the link fields - - A link has the following fields: - - - *href* is the URI of the referenced resource (typically a Web page) - - *rel* contains a single link relationship type. It can be a full URI, - or one of the following predefined values (default=alternate): - - - *alternate* an alternate representation of the entry or feed, for - example a permalink to the html version of the entry, or the - front page of the weblog. - - *enclosure* a related resource which is potentially large in size - and might require special handling, for example an audio or video - recording. - - *related* an document related to the entry or feed. - - *self* the feed itself. - - *via* the source of the information provided in the entry. - - - *type* indicates the media type of the resource. - - *hreflang* indicates the language of the referenced resource. - - *title* human readable information about the link, typically for - display purposes. - - *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: If old links are to be replaced (default: False) - :returns: Current set of link data - - Example:: - - >>> feedgen.link( href='http://example.com/', rel='self') - [{'href':'http://example.com/', 'rel':'self'}] - - ''' - if link is None and kwargs: - link = kwargs - if link is not None: - if replace or self.__atom_link is None: - self.__atom_link = [] - self.__atom_link += ensure_format( - link, - set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), - set(['href']), - {'rel': [ - 'about', 'alternate', 'appendix', 'archives', 'author', - 'bookmark', 'canonical', 'chapter', 'collection', - 'contents', 'copyright', 'create-form', 'current', - 'derivedfrom', 'describedby', 'describes', 'disclosure', - 'duplicate', 'edit', 'edit-form', 'edit-media', - 'enclosure', 'first', 'glossary', 'help', 'hosts', 'hub', - 'icon', 'index', 'item', 'last', 'latest-version', - 'license', 'lrdd', 'memento', 'monitor', 'monitor-group', - 'next', 'next-archive', 'nofollow', 'noreferrer', - 'original', 'payment', 'predecessor-version', 'prefetch', - 'prev', 'preview', 'previous', 'prev-archive', - 'privacy-policy', 'profile', 'related', 'replies', - 'search', 'section', 'self', 'service', 'start', - 'stylesheet', 'subsection', 'successor-version', 'tag', - 'terms-of-service', 'timegate', 'timemap', 'type', 'up', - 'version-history', 'via', 'working-copy', 'working-copy-of' - ]}) - # RSS only needs one URL. We use the first link for RSS: - if len(self.__atom_link) > 0: - self.__rss_link = self.__atom_link[-1]['href'] - # return the set with more information (atom) - return self.__atom_link - - def category(self, category=None, replace=False, **kwargs): - '''Get or set categories that the feed belongs to. - - This method can be called with: - - - the fields of a category as keyword arguments - - the fields of a category as a dictionary - - a list of dictionaries containing the category fields - - A categories has the following fields: - - - *term* identifies the category - - *scheme* identifies the categorization scheme via a URI. - - *label* provides a human-readable label for display - - If a label is present it is used for the RSS feeds. Otherwise the term - is used. The scheme is used for the domain attribute in RSS. - - :param link: Dict or list of dicts with data. - :param replace: Add or replace old data. - :returns: List of category data. - ''' - if category is None and kwargs: - category = kwargs - if category is not None: - if replace or self.__atom_category is None: - self.__atom_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 - # name or if not present the atom:term. The atom:scheme is the - # rss:domain. - self.__rss_category = [] - for cat in self.__atom_category: - rss_cat = {} - rss_cat['value'] = cat.get('label', cat['term']) - if cat.get('scheme'): - rss_cat['domain'] = cat['scheme'] - self.__rss_category.append(rss_cat) - return self.__atom_category - - def cloud(self, domain=None, port=None, path=None, registerProcedure=None, - protocol=None): - '''Set or get the cloud data of the feed. It is an RSS only attribute. - It specifies a web service that supports the rssCloud interface which - can be implemented in HTTP-POST, XML-RPC or SOAP 1.1. - - :param domain: The domain where the webservice can be found. - :param port: The port the webservice listens to. - :param path: The path of the webservice. - :param registerProcedure: The procedure to call. - :param protocol: Can be either HTTP-POST, XML-RPC or SOAP 1.1. - :returns: Dictionary containing the cloud data. - ''' - if domain is not None: - self.__rss_cloud = {'domain': domain, 'port': port, 'path': path, - 'registerProcedure': registerProcedure, - 'protocol': protocol} - return self.__rss_cloud - - def contributor(self, contributor=None, replace=False, **kwargs): - '''Get or set the contributor data of the feed. This is an ATOM only - value. - - This method can be called with: - - the fields of an contributor as keyword arguments - - the fields of an contributor as a dictionary - - a list of dictionaries containing the contributor fields - - An contributor has the following fields: - - *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. - ''' - if contributor is None and kwargs: - contributor = kwargs - if contributor is not None: - if replace or self.__atom_contributor is None: - self.__atom_contributor = [] - self.__atom_contributor += ensure_format( - contributor, set(['name', 'email', 'uri']), set(['name'])) - return self.__atom_contributor - - def generator(self, generator=None, version=None, uri=None): - '''Get or set the generator of the feed which identifies the software - used to generate the feed, for debugging and other purposes. Both the - uri and version attributes are optional and only available in the ATOM - feed. - - :param generator: Software used to create the feed. - :param version: Version of the software. - :param uri: URI the software can be found. - ''' - if generator is not None: - self.__atom_generator = {'value': generator} - if version is not None: - self.__atom_generator['version'] = version - if uri is not None: - self.__atom_generator['uri'] = uri - self.__rss_generator = generator - return self.__atom_generator - - def icon(self, icon=None): - '''Get or set the icon of the feed which is a small image which - provides iconic visual identification for the feed. Icons should be - square. This is an ATOM only value. - - :param icon: URI of the feeds icon. - :returns: URI of the feeds icon. - ''' - if icon is not None: - self.__atom_icon = icon - return self.__atom_icon - - def logo(self, logo=None): - '''Get or set the logo of the feed which is a larger image which - provides visual identification for the feed. Images should be twice as - wide as they are tall. This is an ATOM value but will also set the - rss:image value. - - :param logo: Logo of the feed. - :returns: Logo of the feed. - ''' - if logo is not None: - self.__atom_logo = logo - self.__rss_image = {'url': logo} - return self.__atom_logo - - def image(self, url=None, title=None, link=None, width=None, height=None, - description=None): - '''Set the image of the feed. This element is roughly equivalent to - atom:logo. - - :param url: The URL of a GIF, JPEG or PNG image. - :param title: Describes the image. The default value is the feeds - title. - :param link: URL of the site the image will link to. The default is to - use the feeds first altertate link. - :param width: Width of the image in pixel. The maximum is 144. - :param height: The height of the image. The maximum is 400. - :param description: Title of the link. - :returns: Data of the image as dictionary. - ''' - if url is not None: - self.__rss_image = {'url': url} - if title is not None: - self.__rss_image['title'] = title - if link is not None: - self.__rss_image['link'] = link - if width: - self.__rss_image['width'] = width - if height: - self.__rss_image['height'] = height - self.__atom_logo = url - return self.__rss_image - - def rights(self, rights=None): - '''Get or set the rights value of the feed which conveys information - about rights, e.g. copyrights, held in and over the feed. This ATOM - value will also set rss:copyright. - - :param rights: Rights information of the feed. - ''' - if rights is not None: - self.__atom_rights = rights - self.__rss_copyright = rights - return self.__atom_rights - - def copyright(self, copyright=None): - '''Get or set the copyright notice for content in the channel. This RSS - value will also set the atom:rights value. - - :param copyright: The copyright notice. - :returns: The copyright notice. - ''' - return self.rights(copyright) - - def subtitle(self, subtitle=None): - '''Get or set the subtitle value of the cannel which contains a - human-readable description or subtitle for the feed. This ATOM property - will also set the value for rss:description. - - :param subtitle: The subtitle of the feed. - :returns: The subtitle of the feed. - ''' - if subtitle is not None: - self.__atom_subtitle = subtitle - self.__rss_description = subtitle - return self.__atom_subtitle - - def description(self, description=None): - '''Set and get the description of the feed. This is an RSS only element - which is a phrase or sentence describing the channel. It is mandatory - for RSS feeds. It is roughly the same as atom:subtitle. Thus setting - this will also set atom:subtitle. - - :param description: Description of the channel. - :returns: Description of the channel. - - ''' - return self.subtitle(description) - - def docs(self, docs=None): - '''Get or set the docs value of the feed. This is an RSS only value. It - is a URL that points to the documentation for the format used in the - RSS file. It is probably a pointer to [1]. It is for people who might - stumble across an RSS file on a Web server 25 years from now and wonder - what it is. - - [1]: http://www.rssboard.org/rss-specification - - :param docs: URL of the format documentation. - :returns: URL of the format documentation. - ''' - if docs is not None: - self.__rss_docs = docs - return self.__rss_docs - - def language(self, language=None): - '''Get or set the language of the feed. It indicates the language the - channel is written in. This allows aggregators to group all Italian - language sites, for example, on a single page. This is an RSS only - field. However, this value will also be used to set the xml:lang - property of the ATOM feed node. - The value should be an IETF language tag. - - :param language: Language of the feed. - :returns: Language of the feed. - ''' - if language is not None: - self.__rss_language = language - self.__atom_feed_xml_lang = language - return self.__rss_language - - def managingEditor(self, managingEditor=None): - '''Set or get the value for managingEditor which is the email address - for person responsible for editorial content. This is a RSS only - value. - - :param managingEditor: Email address of the managing editor. - :returns: Email address of the managing editor. - ''' - if managingEditor is not None: - self.__rss_managingEditor = managingEditor - return self.__rss_managingEditor - - def pubDate(self, pubDate=None): - '''Set or get the publication date for the content in the channel. For - example, the New York Times publishes on a daily basis, the publication - date flips once every 24 hours. That's when the pubDate of the channel - changes. - - The value can either be a string which will automatically be parsed or - a datetime.datetime object. In any case it is necessary that the value - include timezone information. - - This will set both atom:updated and rss:lastBuildDate. - - :param pubDate: The publication date. - :returns: Publication date as datetime.datetime - ''' - if pubDate is not None: - if isinstance(pubDate, string_types): - pubDate = dateutil.parser.parse(pubDate) - if not isinstance(pubDate, datetime): - raise ValueError('Invalid datetime format') - if pubDate.tzinfo is None: - raise ValueError('Datetime object has no timezone info') - self.__rss_pubDate = pubDate - - return self.__rss_pubDate - - def rating(self, rating=None): - '''Set and get the PICS rating for the channel. It is an RSS only - value. - ''' - if rating is not None: - self.__rss_rating = rating - return self.__rss_rating - - def skipHours(self, hours=None, replace=False): - '''Set or get the value of skipHours, a hint for aggregators telling - them which hours they can skip. This is an RSS only value. - - 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. - ''' - if hours is not None: - if not (isinstance(hours, list) or isinstance(hours, set)): - hours = [hours] - for h in hours: - if h not in range(24): - raise ValueError('Invalid hour %s' % h) - if replace or not self.__rss_skipHours: - self.__rss_skipHours = set() - self.__rss_skipHours |= set(hours) - return self.__rss_skipHours - - def skipDays(self, days=None, replace=False): - '''Set or get the value of skipDays, a hint for aggregators telling - them which days they can skip This is an RSS only value. - - 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. - ''' - if days is not None: - if not (isinstance(days, list) or isinstance(days, set)): - days = [days] - for d in days: - if d not in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', - 'Friday', 'Saturday', 'Sunday']: - raise ValueError('Invalid day %s' % d) - if replace or not self.__rss_skipDays: - self.__rss_skipDays = set() - self.__rss_skipDays |= set(days) - return self.__rss_skipDays - - def textInput(self, title=None, description=None, name=None, link=None): - '''Get or set the value of textInput. This is an RSS only field. The - purpose of the element is something of a mystery. You can - use it to specify a search engine box. Or to allow a reader to provide - feedback. Most aggregators ignore it. - - :param title: The label of the Submit button in the text input area. - :param description: Explains the text input area. - :param name: The name of the text object in the text input area. - :param link: The URL of the CGI script that processes text input - requests. - :returns: Dictionary containing textInput values. - ''' - if title is not None: - self.__rss_textInput = {} - self.__rss_textInput['title'] = title - self.__rss_textInput['description'] = description - self.__rss_textInput['name'] = name - self.__rss_textInput['link'] = link - return self.__rss_textInput - - def ttl(self, ttl=None): - '''Get or set the ttl value. It is an RSS only element. ttl stands for - time to live. It's a number of minutes that indicates how long a - channel can be cached before refreshing from the source. - - :param ttl: Integer value indicating how long the channel may be - cached. - :returns: Time to live. - ''' - if ttl is not None: - self.__rss_ttl = int(ttl) - return self.__rss_ttl - - def webMaster(self, webMaster=None): - '''Get and set the value of webMaster, which represents the email - address for the person responsible for technical issues relating to the - feed. This is an RSS only value. - - :param webMaster: Email address of the webmaster. - :returns: Email address of the webmaster. - ''' - if webMaster is not None: - self.__rss_webMaster = webMaster - return self.__rss_webMaster - - def add_entry(self, feedEntry=None, order='prepend'): - '''This method will add a new entry to the feed. If the feedEntry - argument is omittet a new Entry object is created automatically. This - is the preferred way to add new entries to a feed. - - :param feedEntry: FeedEntry object to add. - :param order: If `prepend` is chosen, the entry will be inserted - at the beginning of the feed. If `append` is chosen, - the entry will be appended to the feed. - (default: `prepend`). - :returns: FeedEntry object created or passed to this function. - - Example:: - - ... - >>> entry = feedgen.add_entry() - >>> entry.title('First feed entry') - - ''' - if feedEntry is None: - feedEntry = FeedEntry() - - version = sys.version_info[0] - - if version == 2: - items = self.__extensions.iteritems() - else: - items = self.__extensions.items() - - # Try to load extensions: - for extname, ext in items: - try: - feedEntry.register_extension(extname, - ext['extension_class_entry'], - ext['atom'], - ext['rss']) - except ImportError: - pass - - if order == 'prepend': - self.__feed_entries.insert(0, feedEntry) - else: - self.__feed_entries.append(feedEntry) - return feedEntry - - def add_item(self, item=None): - '''This method will add a new item to the feed. If the item argument is - omittet a new FeedEntry object is created automatically. This is just - another name for add_entry(...) - ''' - return self.add_entry(item) - - 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. - :returns: List ob all feed entries. - ''' - if entry is not None: - if not isinstance(entry, list): - entry = [entry] - if replace: - self.__feed_entries = [] - - version = sys.version_info[0] - - if version == 2: - items = self.__extensions.iteritems() - else: - items = self.__extensions.items() - - # Try to load extensions: - for e in entry: - for extname, ext in items: - try: - e.register_extension(extname, - ext['extension_class_entry'], - ext['atom'], ext['rss']) - except ImportError: - pass - - self.__feed_entries += entry - return self.__feed_entries - - def item(self, item=None, replace=False): - '''Get or set feed items. This is just another name for entry(...) - ''' - return self.entry(item, replace) - - def remove_entry(self, entry): - '''Remove a single entry from the feed. This method accepts both the - FeedEntry object to remove or the index of the entry as argument. - - :param entry: Entry or index of entry to remove. - ''' - if isinstance(entry, FeedEntry): - 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 - 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:] - feedsupmod = __import__('feedgen.ext.%s' % name) - feedextmod = getattr(feedsupmod.ext, name) - try: - entrysupmod = __import__('feedgen.ext.%s_entry' % name) - entryextmod = getattr(entrysupmod.ext, name + '_entry') - except ImportError: - # Use FeedExtension module instead - entrysupmod = feedsupmod - entryextmod = feedextmod - feedext = getattr(feedextmod, extname + 'Extension') - try: - entryext = getattr(entryextmod, extname + 'EntryExtension') - except AttributeError: - entryext = None - self.register_extension(name, feedext, entryext, atom, rss) - - def register_extension(self, namespace, extension_class_feed=None, - extension_class_entry=None, atom=True, rss=True): - '''Registers an extension by class. - - :param namespace: namespace for the extension - :param extension_class_feed: Class of the feed extension to load. - :param extension_class_entry: Class of the entry 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 - # `load_extension` ignores the "Extension" suffix. - if not isinstance(self.__extensions, dict): - self.__extensions = {} - if namespace in self.__extensions.keys(): - raise ImportError('Extension already loaded') - - # Load extension - extinst = extension_class_feed() - setattr(self, namespace, extinst) - - # `load_extension` registry - self.__extensions[namespace] = { - 'inst': extinst, - 'extension_class_feed': extension_class_feed, - 'extension_class_entry': extension_class_entry, - 'atom': atom, - 'rss': rss - } - - # Try to load the extension for already existing entries: - for entry in self.__feed_entries: - try: - entry.register_extension(namespace, - extension_class_entry, - atom, - rss) - except ImportError: - pass diff --git a/util.py b/util.py deleted file mode 100644 index 8b4e6e5..0000000 --- a/util.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.util - ~~~~~~~~~~~~ - - This file contains helper functions for the feed generator module. - - :copyright: 2013, Lars Kiesow - :license: FreeBSD and LGPL, see license.* for more details. -''' -import locale -import sys -import lxml # nosec - we configure a safe parser below - -# Configure a safe parser which does not allow XML entity expansion -parser = lxml.etree.XMLParser( - attribute_defaults=False, - dtd_validation=False, - load_dtd=False, - no_network=True, - recover=False, - remove_pis=True, - resolve_entities=False, - huge_tree=False) - - -def xml_fromstring(xmlstring): - return lxml.etree.fromstring(xmlstring, parser) # nosec - safe parser - - -def xml_elem(name, parent=None, **kwargs): - if parent is not None: - return lxml.etree.SubElement(parent, name, **kwargs) - return lxml.etree.Element(name, **kwargs) - - -def ensure_format(val, allowed, required, allowed_values=None, defaults=None): - '''Takes a dictionary or a list of dictionaries and check if all keys are in - the set of allowed keys, if all required keys are present and if the values - of a specific key are ok. - - :param val: Dictionaries to check. - :param allowed: Set of allowed keys. - :param required: Set of required keys. - :param allowed_values: Dictionary with keys and sets of their allowed - values. - :param defaults: Dictionary with default values. - :returns: List of checked dictionaries. - ''' - if not val: - return [] - if allowed_values is None: - allowed_values = {} - if defaults is None: - defaults = {} - # Make shure that we have a list of dicts. Even if there is only one. - if not isinstance(val, list): - val = [val] - for elem in val: - if not isinstance(elem, dict): - raise ValueError('Invalid data (value is no dictionary)') - # Set default values - - version = sys.version_info[0] - - if version == 2: - items = defaults.iteritems() - else: - items = defaults.items() - - for k, v in items: - elem[k] = elem.get(k, v) - if not set(elem.keys()) <= allowed: - raise ValueError('Data contains invalid keys') - if not set(elem.keys()) >= required: - raise ValueError('Data contains not all required keys') - - if version == 2: - values = allowed_values.iteritems() - else: - values = allowed_values.items() - - for k, v in values: - if elem.get(k) and not elem[k] in v: - raise ValueError('Invalid value for %s' % k) - return val - - -def formatRFC2822(date): - '''Make sure the locale setting do not interfere with the time format. - ''' - old = locale.setlocale(locale.LC_ALL) - locale.setlocale(locale.LC_ALL, 'C') - date = date.strftime('%a, %d %b %Y %H:%M:%S %z') - locale.setlocale(locale.LC_ALL, old) - return date diff --git a/version.py b/version.py deleted file mode 100644 index 2a59ec0..0000000 --- a/version.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -''' - feedgen.version - ~~~~~~~~~~~~~~~ - - :copyright: 2013-2018, Lars Kiesow - - :license: FreeBSD and LGPL, see license.* for more details. - -''' - -'Version of python-feedgen represented as tuple' -version = (0, 9, 0) - - -'Version of python-feedgen represented as string' -version_str = '.'.join([str(x) for x in version]) - -version_major = version[:1] -version_minor = version[:2] -version_full = version - -version_major_str = '.'.join([str(x) for x in version_major]) -version_minor_str = '.'.join([str(x) for x in version_minor]) -version_full_str = '.'.join([str(x) for x in version_full])