Flake8 Compatibility

This patch makes the feedgen flake8 compatible, fixing some minor issues
along the way. Most noticeable, this switches from tabs to spaces.

Signed-off-by: Lars Kiesow <lkiesow@uos.de>
This commit is contained in:
Lars Kiesow 2016-12-21 02:04:24 +01:00
parent ccf18502bc
commit 444855a248
No known key found for this signature in database
GPG key ID: 5DAFE8D9C823CE73
19 changed files with 3553 additions and 3572 deletions

View file

@ -53,3 +53,4 @@ test:
python -m unittest tests.test_entry python -m unittest tests.test_entry
python -m unittest tests.test_extension python -m unittest tests.test_extension
@rm -f tmp_Atomfeed.xml tmp_Rssfeed.xml @rm -f tmp_Atomfeed.xml tmp_Rssfeed.xml
flake8 $$(find setup.py tests feedgen -name '*.py')

View file

@ -3,7 +3,10 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys, os, time, codecs, re import sys
import os
import codecs
import re
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
@ -13,13 +16,13 @@ sys.path.insert(0, os.path.abspath('.'))
import feedgen.version import feedgen.version
# -- General configuration ----------------------------------------------------- # -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0' #needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [ extensions = [
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
'sphinx.ext.coverage', 'sphinx.ext.coverage',
@ -65,7 +68,8 @@ release = feedgen.version.version_full_str
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = ['_build'] exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None #default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text. # If true, '()' will be appended to :func: etc. cross-reference text.
@ -86,7 +90,7 @@ pygments_style = 'sphinx'
#modindex_common_prefix = [] #modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
@ -169,24 +173,24 @@ html_static_path = ['_static']
htmlhelp_basename = 'pyFeedGen' htmlhelp_basename = 'pyFeedGen'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output -------------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', #'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt', #'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#'preamble': '', #'preamble': '',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples (source start
# (source start file, target name, title, author, documentclass [howto/manual]). # file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'pyFeedGen.tex', u'pyFeedGen Documentation', ('index', 'pyFeedGen.tex', u'pyFeedGen Documentation', u'Lars Kiesow',
u'Lars Kiesow', 'manual'), 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -210,7 +214,7 @@ latex_documents = [
#latex_domain_indices = True #latex_domain_indices = True
# -- Options for manual page output -------------------------------------------- # -- Options for manual page output -------------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
@ -223,7 +227,7 @@ man_pages = [
#man_show_urls = False #man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------ # -- Options for Texinfo output -----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples # Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author, # (source start file, target name, title, author,
@ -248,28 +252,11 @@ texinfo_documents = [
intersphinx_mapping = {'http://docs.python.org/': None} intersphinx_mapping = {'http://docs.python.org/': None}
# Ugly way of setting tabsize
import re
def process_docstring(app, what, name, obj, options, lines):
'''
spaces_pat = re.compile(r"(?<= )( {8})")
ll = []
for l in lines:
ll.append(spaces_pat.sub(" ",l))
'''
spaces_pat = re.compile(r"^ *")
ll = []
for l in lines:
spacelen = len(spaces_pat.search(l).group(0))
newlen = (spacelen % 8) + (spacelen / 8 * 4)
ll.append( (' '*newlen) + l.lstrip(' ') )
lines[:] = ll
# Include the GitHub readme file in index.rst # Include the GitHub readme file in index.rst
r = re.compile(r'\[`*([^\]`]+)`*\]\(([^\)]+)\)') r = re.compile(r'\[`*([^\]`]+)`*\]\(([^\)]+)\)')
r2 = re.compile(r'.. include-github-readme') r2 = re.compile(r'.. include-github-readme')
def substitute_link(app, docname, text): def substitute_link(app, docname, text):
if docname == 'index': if docname == 'index':
readme_text = '' readme_text = ''
@ -279,5 +266,4 @@ def substitute_link(app, docname, text):
def setup(app): def setup(app):
app.connect('autodoc-process-docstring', process_docstring)
app.connect('source-read', substitute_link) app.connect('source-read', substitute_link)

View file

@ -39,15 +39,16 @@
Example:: Example::
>>> fg.contributor( name='John Doe', email='jdoe@example.com' ) >>> fg.contributor(name='John Doe', email='jdoe@example.com' )
>>> fg.contributor({'name':'John Doe', 'email':'jdoe@example.com'}) >>> 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 Generate the Feed
----------------- -----------------
After that you can generate both RSS or ATOM by calling the respective method:: 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 >>> atomfeed = fg.atom_str(pretty=True) # Get the ATOM feed as string
>>> rssfeed = fg.rss_str(pretty=True) # Get the RSS feed as string >>> rssfeed = fg.rss_str(pretty=True) # Get the RSS feed as string
@ -77,8 +78,8 @@
Extensions Extensions
---------- ----------
The FeedGenerator supports extension to include additional data into the XML The FeedGenerator supports extension to include additional data into the
structure of the feeds. Extensions can be loaded like this:: XML structure of the feeds. Extensions can be loaded like this::
>>> fg.load_extension('someext', atom=True, rss=True) >>> fg.load_extension('someext', atom=True, rss=True)
@ -94,14 +95,14 @@
`ext/someext_entry.py` which is suggested especially for large extensions. `ext/someext_entry.py` which is suggested especially for large extensions.
The parameters `atom` and `rss` tell the FeedGenerator if the 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 should only be used for either ATOM or RSS feeds. The default value for
parameters is true which means that the extension would be used for both both parameters is true which means that the extension would be used for
kinds of feeds. both kinds of feeds.
**Example: Produceing a Podcast** **Example: Produceing a Podcast**
One extension already provided is the podcast extension. A podcast is an RSS One extension already provided is the podcast extension. A podcast is an
feed with some additional elements for ITunes. RSS feed with some additional elements for ITunes.
To produce a podcast simply load the `podcast` extension:: To produce a podcast simply load the `podcast` extension::

View file

@ -11,37 +11,46 @@
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
import sys import sys
USAGE = '''
Usage: python -m feedgen [OPTION]
Use one of the following options:
File options:
<file>.atom -- Generate ATOM test feed
<file>.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): def print_enc(s):
'''Print function compatible with both python2 and python3 accepting strings '''Print function compatible with both python2 and python3 accepting strings
and byte arrays. and byte arrays.
''' '''
if sys.version_info[0] >= 3: if sys.version_info[0] >= 3:
print(s.decode('utf-8') if type(s) == type(b'') else s) print(s.decode('utf-8') if isinstance(s, bytes) else s)
else: else:
print(s) print(s)
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) != 2 or not ( if len(sys.argv) != 2 or not (
sys.argv[1].endswith('rss') \ sys.argv[1].endswith('rss') or
or sys.argv[1].endswith('atom') \ sys.argv[1].endswith('atom') or
or sys.argv[1].endswith('torrent') \ sys.argv[1] == 'torrent' or
or sys.argv[1].endswith('podcast') ): sys.argv[1] == 'podcast'):
print_enc ('Usage: %s ( <file>.atom | atom | <file>.rss | rss | podcast | torrent )' % \ print(USAGE)
'python -m feedgen')
print_enc ('')
print_enc (' atom -- Generate ATOM test output and print it to stdout.')
print_enc (' rss -- Generate RSS test output and print it to stdout.')
print_enc (' <file>.atom -- Generate ATOM test feed and write it to file.atom.')
print_enc (' <file>.rss -- Generate RSS test teed and write it to file.rss.')
print_enc (' podcast -- Generate Podcast test output and print it to stdout.')
print_enc (' dc.atom -- Generate DC extension test output (atom format) and print it to stdout.')
print_enc (' dc.rss -- Generate DC extension test output (rss format) and print it to stdout.')
print_enc (' syndication.atom -- Generate syndication extension test output (atom format) and print it to stdout.')
print_enc (' syndication.rss -- Generate syndication extension test output (rss format) and print it to stdout.')
print_enc (' torrent -- Generate Torrent test output and print it to stdout.')
print_enc ('')
exit() exit()
arg = sys.argv[1] arg = sys.argv[1]
@ -49,34 +58,34 @@ if __name__ == '__main__':
fg = FeedGenerator() fg = FeedGenerator()
fg.id('http://lernfunk.de/_MEDIAID_123') fg.id('http://lernfunk.de/_MEDIAID_123')
fg.title('Testfeed') fg.title('Testfeed')
fg.author( {'name':'Lars Kiesow','email':'lkiesow@uos.de'} ) fg.author({'name': 'Lars Kiesow', 'email': 'lkiesow@uos.de'})
fg.link( href='http://example.com', rel='alternate' ) fg.link(href='http://example.com', rel='alternate')
fg.category(term='test') fg.category(term='test')
fg.contributor( name='Lars Kiesow', email='lkiesow@uos.de' ) fg.contributor(name='Lars Kiesow', email='lkiesow@uos.de')
fg.contributor( name='John Doe', email='jdoe@example.com' ) fg.contributor(name='John Doe', email='jdoe@example.com')
fg.icon('http://ex.com/icon.jpg') fg.icon('http://ex.com/icon.jpg')
fg.logo('http://ex.com/logo.jpg') fg.logo('http://ex.com/logo.jpg')
fg.rights('cc-by') fg.rights('cc-by')
fg.subtitle('This is a cool feed!') fg.subtitle('This is a cool feed!')
fg.link( href='http://larskiesow.de/test.atom', rel='self' ) fg.link(href='http://larskiesow.de/test.atom', rel='self')
fg.language('de') fg.language('de')
fe = fg.add_entry() fe = fg.add_entry()
fe.id('http://lernfunk.de/_MEDIAID_123#1') fe.id('http://lernfunk.de/_MEDIAID_123#1')
fe.title('First Element') fe.title('First Element')
fe.content('''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tamen fe.content('''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tamen
aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si ista aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si
mala sunt, placet. Aut etiam, ut vestitum, sic sententiam habeas aliam ista mala sunt, placet. Aut etiam, ut vestitum, sic sententiam
domesticam, aliam forensem, ut in fronte ostentatio sit, intus veritas habeas aliam domesticam, aliam forensem, ut in fronte ostentatio
occultetur? Cum id fugiunt, re eadem defendunt, quae Peripatetici, sit, intus veritas occultetur? Cum id fugiunt, re eadem defendunt,
verba.''') quae Peripatetici, verba.''')
fe.summary(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit…') fe.summary(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit…')
fe.link( href='http://example.com', rel='alternate' ) fe.link(href='http://example.com', rel='alternate')
fe.author( name='Lars Kiesow', email='lkiesow@uos.de' ) fe.author(name='Lars Kiesow', email='lkiesow@uos.de')
if arg == 'atom': if arg == 'atom':
print_enc (fg.atom_str(pretty=True)) print_enc(fg.atom_str(pretty=True))
elif arg == 'rss': elif arg == 'rss':
print_enc (fg.rss_str(pretty=True)) print_enc(fg.rss_str(pretty=True))
elif arg == 'podcast': elif arg == 'podcast':
# Load the podcast extension. It will automatically be loaded for all # Load the podcast extension. It will automatically be loaded for all
# entries in the feed, too. Thus also for our “fe”. # entries in the feed, too. Thus also for our “fe”.
@ -87,30 +96,32 @@ if __name__ == '__main__':
fg.podcast.itunes_complete('no') fg.podcast.itunes_complete('no')
fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss') fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss')
fg.podcast.itunes_owner('John Doe', 'john@example.com') fg.podcast.itunes_owner('John Doe', 'john@example.com')
fg.podcast.itunes_summary('Lorem ipsum dolor sit amet, ' + \ fg.podcast.itunes_summary('Lorem ipsum dolor sit amet, consectetur ' +
'consectetur adipiscing elit. ' + \ 'adipiscing elit. Verba tu fingas et ea ' +
'Verba tu fingas et ea dicas, quae non sentias?') 'dicas, quae non sentias?')
fe.podcast.itunes_author('Lars Kiesow') fe.podcast.itunes_author('Lars Kiesow')
print_enc (fg.rss_str(pretty=True)) print_enc(fg.rss_str(pretty=True))
elif arg == 'torrent': elif arg == 'torrent':
fg.load_extension('torrent') fg.load_extension('torrent')
fe.link( href='http://somewhere.behind.the.sea/torrent/debian-8.4.0-i386-netint.iso.torrent', rel='alternate', type='application/x-bittorrent, length=1000' ) 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.filename('debian-8.4.0-i386-netint.iso.torrent')
fe.torrent.infohash('7661229811ef32014879ceedcdf4a48f256c88ba') fe.torrent.infohash('7661229811ef32014879ceedcdf4a48f256c88ba')
fe.torrent.contentlength('331350016') fe.torrent.contentlength('331350016')
fe.torrent.seeds('789') fe.torrent.seeds('789')
fe.torrent.peers('456') fe.torrent.peers('456')
fe.torrent.verified('123') fe.torrent.verified('123')
print_enc (fg.rss_str(pretty=True)) print_enc(fg.rss_str(pretty=True))
elif arg.startswith('dc.'): elif arg.startswith('dc.'):
fg.load_extension('dc') fg.load_extension('dc')
fg.dc.dc_contributor('Lars Kiesow') fg.dc.dc_contributor('Lars Kiesow')
if arg.endswith('.atom'): if arg.endswith('.atom'):
print_enc (fg.atom_str(pretty=True)) print_enc(fg.atom_str(pretty=True))
else: else:
print_enc (fg.rss_str(pretty=True)) print_enc(fg.rss_str(pretty=True))
elif arg.startswith('syndication'): elif arg.startswith('syndication'):
fg.load_extension('syndication') fg.load_extension('syndication')
@ -118,9 +129,9 @@ if __name__ == '__main__':
fg.syndication.update_frequency(2) fg.syndication.update_frequency(2)
fg.syndication.update_base('2000-01-01T12:00+00:00') fg.syndication.update_base('2000-01-01T12:00+00:00')
if arg.endswith('.rss'): if arg.endswith('.rss'):
print_enc (fg.rss_str(pretty=True)) print_enc(fg.rss_str(pretty=True))
else: else:
print_enc (fg.atom_str(pretty=True)) print_enc(fg.atom_str(pretty=True))
elif arg.endswith('atom'): elif arg.endswith('atom'):
fg.atom_file(arg) fg.atom_file(arg)

View file

@ -4,4 +4,4 @@ import sys
if sys.version_info[0] >= 3: if sys.version_info[0] >= 3:
string_types = str string_types = str
else: else:
string_types = basestring string_types = basestring # noqa: F821

View file

@ -58,11 +58,10 @@ class FeedEntry(object):
self.__extensions = {} self.__extensions = {}
self.__extensions_register = {} self.__extensions_register = {}
def atom_entry(self, extensions=True): def atom_entry(self, extensions=True):
'''Create an ATOM entry and return it.''' '''Create an ATOM entry and return it.'''
entry = etree.Element('entry') entry = etree.Element('entry')
if not ( self.__atom_id and self.__atom_title and self.__atom_updated ): if not (self.__atom_id and self.__atom_title and self.__atom_updated):
raise ValueError('Required fields not set') raise ValueError('Required fields not set')
id = etree.SubElement(entry, 'id') id = etree.SubElement(entry, 'id')
id.text = self.__atom_id id.text = self.__atom_id
@ -71,12 +70,13 @@ class FeedEntry(object):
updated = etree.SubElement(entry, 'updated') updated = etree.SubElement(entry, 'updated')
updated.text = self.__atom_updated.isoformat() updated.text = self.__atom_updated.isoformat()
# An entry must contain an alternate link if there is no content element. # An entry must contain an alternate link if there is no content
# element.
if not self.__atom_content: if not self.__atom_content:
if not True in [ l.get('rel') == 'alternate' \ links = self.__atom_link or []
for l in 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 ' raise ValueError('Entry must contain an alternate link or ' +
+ 'a content element.') 'a content element.')
# Add author elements # Add author elements
for a in self.__atom_author or []: for a in self.__atom_author or []:
@ -101,21 +101,24 @@ class FeedEntry(object):
elif self.__atom_content.get('content'): elif self.__atom_content.get('content'):
# Surround xhtml with a div tag, parse it and embed it # Surround xhtml with a div tag, parse it and embed it
if type == 'xhtml': if type == 'xhtml':
content.append(etree.fromstring('''<div content.append(etree.fromstring(
xmlns="http://www.w3.org/1999/xhtml">%s</div>''' % \ '<div xmlns="http://www.w3.org/1999/xhtml">' +
self.__atom_content.get('content'))) self.__atom_content.get('content') + '</div>'))
elif type == 'CDATA': elif type == 'CDATA':
content.text = etree.CDATA(self.__atom_content.get('content')) content.text = etree.CDATA(
self.__atom_content.get('content'))
# Emed the text in escaped form # Emed the text in escaped form
elif not type or type.startswith('text') or type == 'html': elif not type or type.startswith('text') or type == 'html':
content.text = self.__atom_content.get('content') content.text = self.__atom_content.get('content')
# Parse XML and embed it # Parse XML and embed it
elif type.endswith('/xml') or type.endswith('+xml'): elif type.endswith('/xml') or type.endswith('+xml'):
content.append(etree.fromstring(self.__atom_content['content'])) content.append(etree.fromstring(
self.__atom_content['content']))
# Everything else should be included base64 encoded # Everything else should be included base64 encoded
else: else:
raise ValueError('base64 encoded content is not supported at the moment.' raise ValueError('base64 encoded content is not ' +
+ 'If you are interested , please file a bug report.') 'supported at the moment. Pull requests' +
' adding support are welcome.')
# Add type description of the content # Add type description of the content
if type: if type:
content.attrib['type'] = type content.attrib['type'] = type
@ -149,7 +152,7 @@ class FeedEntry(object):
# Atom requires a name. Skip elements without. # Atom requires a name. Skip elements without.
if not c.get('name'): if not c.get('name'):
continue continue
contrib = etree.SubElement(feed, 'contributor') contrib = etree.SubElement(entry, 'contributor')
name = etree.SubElement(contrib, 'name') name = etree.SubElement(contrib, 'name')
name.text = c.get('name') name.text = c.get('name')
if c.get('email'): if c.get('email'):
@ -164,7 +167,7 @@ class FeedEntry(object):
published.text = self.__atom_published.isoformat() published.text = self.__atom_published.isoformat()
if self.__atom_rights: if self.__atom_rights:
rights = etree.SubElement(feed, 'rights') rights = etree.SubElement(entry, 'rights')
rights.text = self.__atom_rights rights.text = self.__atom_rights
if extensions: if extensions:
@ -174,11 +177,12 @@ class FeedEntry(object):
return entry return entry
def rss_entry(self, extensions=True): def rss_entry(self, extensions=True):
'''Create a RSS item and return it.''' '''Create a RSS item and return it.'''
entry = etree.Element('item') entry = etree.Element('item')
if not ( self.__rss_title or self.__rss_description or self.__rss_content): if not (self.__rss_title or
self.__rss_description or
self.__rss_content):
raise ValueError('Required fields not set') raise ValueError('Required fields not set')
if self.__rss_title: if self.__rss_title:
title = etree.SubElement(entry, 'title') title = etree.SubElement(entry, 'title')
@ -189,10 +193,11 @@ class FeedEntry(object):
if self.__rss_description and self.__rss_content: if self.__rss_description and self.__rss_content:
description = etree.SubElement(entry, 'description') description = etree.SubElement(entry, 'description')
description.text = self.__rss_description description.text = self.__rss_description
content = etree.SubElement(entry, '{%s}encoded' % XMLNS_CONTENT = 'http://purl.org/rss/1.0/modules/content/'
'http://purl.org/rss/1.0/modules/content/') content = etree.SubElement(entry, '{%s}encoded' % XMLNS_CONTENT)
content.text = etree.CDATA(self.__rss_content['content']) \ content.text = etree.CDATA(self.__rss_content['content']) \
if self.__rss_content.get('type', '') == 'CDATA' else self.__rss_content['content'] if self.__rss_content.get('type', '') == 'CDATA' \
else self.__rss_content['content']
elif self.__rss_description: elif self.__rss_description:
description = etree.SubElement(entry, 'description') description = etree.SubElement(entry, 'description')
description.text = self.__rss_description description.text = self.__rss_description
@ -230,8 +235,6 @@ class FeedEntry(object):
return entry return entry
def title(self, title=None): def title(self, title=None):
'''Get or set the title value of the entry. It should contain a human '''Get or set the title value of the entry. It should contain a human
readable title for the entry. Title is mandatory for both ATOM and RSS readable title for the entry. Title is mandatory for both ATOM and RSS
@ -240,49 +243,47 @@ class FeedEntry(object):
:param title: The new title of the entry. :param title: The new title of the entry.
:returns: The entriess title. :returns: The entriess title.
''' '''
if not title is None: if title is not None:
self.__atom_title = title self.__atom_title = title
self.__rss_title = title self.__rss_title = title
return self.__atom_title return self.__atom_title
def id(self, id=None): def id(self, id=None):
'''Get or set the entry id which identifies the entry using a universally '''Get or set the entry id which identifies the entry using a
unique and permanent URI. Two entries in a feed can have the same value universally unique and permanent URI. Two entries in a feed can have
for id if they represent the same entry at different points in time. This the same value for id if they represent the same entry at different
method will also set rss:guid. Id is mandatory for an ATOM entry. points in time. This method will also set rss:guid. Id is mandatory
for an ATOM entry.
:param id: New Id of the entry. :param id: New Id of the entry.
:returns: Id of the entry. :returns: Id of the entry.
''' '''
if not id is None: if id is not None:
self.__atom_id = id self.__atom_id = id
self.__rss_guid = id self.__rss_guid = id
return self.__atom_id return self.__atom_id
def guid(self, guid=None): def guid(self, guid=None):
'''Get or set the entries guid which is a string that uniquely identifies '''Get or set the entries guid which is a string that uniquely
the item. This will also set atom:id. identifies the item. This will also set atom:id.
:param guid: Id of the entry. :param guid: Id of the entry.
:returns: Id of the entry. :returns: Id of the entry.
''' '''
return self.id(guid) return self.id(guid)
def updated(self, updated=None): def updated(self, updated=None):
'''Set or get the updated value which indicates the last time the entry '''Set or get the updated value which indicates the last time the entry
was modified in a significant way. was modified in a significant way.
The value can either be a string which will automatically be parsed or a The value can either be a string which will automatically be parsed or
datetime.datetime object. In any case it is necessary that the value a datetime.datetime object. In any case it is necessary that the value
include timezone information. include timezone information.
:param updated: The modification date. :param updated: The modification date.
:returns: Modification date as datetime.datetime :returns: Modification date as datetime.datetime
''' '''
if not updated is None: if updated is not None:
if isinstance(updated, string_types): if isinstance(updated, string_types):
updated = dateutil.parser.parse(updated) updated = dateutil.parser.parse(updated)
if not isinstance(updated, datetime): if not isinstance(updated, datetime):
@ -294,11 +295,10 @@ class FeedEntry(object):
return self.__atom_updated return self.__atom_updated
def author(self, author=None, replace=False, **kwargs): def author(self, author=None, replace=False, **kwargs):
'''Get or set autor data. An author element is a dict containing a name, '''Get or set autor data. An author element is a dict containing a
an email adress and a uri. Name is mandatory for ATOM, email is mandatory name, an email adress and a uri. Name is mandatory for ATOM, email is
for RSS. mandatory for RSS.
This method can be called with: This method can be called with:
- the fields of an author as keyword arguments - the fields of an author as keyword arguments
@ -315,36 +315,36 @@ class FeedEntry(object):
Example:: Example::
>>> author( { 'name':'John Doe', 'email':'jdoe@example.com' } ) >>> author({'name':'John Doe', 'email':'jdoe@example.com'})
[{'name':'John Doe','email':'jdoe@example.com'}] [{'name':'John Doe','email':'jdoe@example.com'}]
>>> author([{'name':'Mr. X'},{'name':'Max'}]) >>> author([{'name': 'Mr. X'}, {'name': 'Max'}])
[{'name':'John Doe','email':'jdoe@example.com'}, [{'name':'John Doe','email':'jdoe@example.com'},
{'name':'John Doe'}, {'name':'Max'}] {'name':'John Doe'}, {'name':'Max'}]
>>> author( name='John Doe', email='jdoe@example.com', replace=True ) >>> author(name='John Doe', email='jdoe@example.com', replace=True)
[{'name':'John Doe','email':'jdoe@example.com'}] [{'name':'John Doe','email':'jdoe@example.com'}]
''' '''
if author is None and kwargs: if author is None and kwargs:
author = kwargs author = kwargs
if not author is None: if author is not None:
if replace or self.__atom_author is None: if replace or self.__atom_author is None:
self.__atom_author = [] self.__atom_author = []
self.__atom_author += ensure_format( author, self.__atom_author += ensure_format(author,
set(['name', 'email', 'uri']), set(['name'])) set(['name', 'email', 'uri']),
set(['name']))
self.__rss_author = [] self.__rss_author = []
for a in self.__atom_author: for a in self.__atom_author:
if a.get('email'): if a.get('email'):
self.__rss_author.append('%s (%s)' % ( a['email'], a['name'] )) self.__rss_author.append('%(email)s (%(name)s)' % a)
return self.__atom_author return self.__atom_author
def content(self, content=None, src=None, type=None): def content(self, content=None, src=None, type=None):
'''Get or set the cntent of the entry which contains or links to the '''Get or set the cntent of the entry which contains or links to the
complete content of the entry. Content must be provided for ATOM entries complete content of the entry. Content must be provided for ATOM
if there is no alternate link, and should be provided if there is no entries if there is no alternate link, and should be provided if there
summary. If the content is set (not linked) it will also set is no summary. If the content is set (not linked) it will also set
rss:description. rss:description.
:param content: The content of the feed entry. :param content: The content of the feed entry.
@ -352,20 +352,20 @@ class FeedEntry(object):
:param type: If type is CDATA content would not be escaped. :param type: If type is CDATA content would not be escaped.
:returns: Content element of the entry. :returns: Content element of the entry.
''' '''
if not src is None: if src is not None:
self.__atom_content = {'src':src} self.__atom_content = {'src': src}
elif not content is None: elif content is not None:
self.__atom_content = {'content':content} self.__atom_content = {'content': content}
self.__rss_content = {'content':content} self.__rss_content = {'content': content}
if not type is None: if type is not None:
self.__atom_content['type'] = type self.__atom_content['type'] = type
self.__rss_content['type'] = type self.__rss_content['type'] = type
return self.__atom_content return self.__atom_content
def link(self, link=None, replace=False, **kwargs): def link(self, link=None, replace=False, **kwargs):
'''Get or set link data. An link element is a dict with the fields href, '''Get or set link data. An link element is a dict with the fields
rel, type, hreflang, title, and length. Href is mandatory for ATOM. href, rel, type, hreflang, title, and length. Href is mandatory for
ATOM.
This method can be called with: This method can be called with:
- the fields of a link as keyword arguments - the fields of a link as keyword arguments
@ -379,8 +379,8 @@ class FeedEntry(object):
or one of the following predefined values (default=alternate): or one of the following predefined values (default=alternate):
- *alternate* an alternate representation of the entry or feed, for - *alternate* an alternate representation of the entry or feed, for
example a permalink to the html version of the entry, or the front example a permalink to the html version of the entry, or the
page of the weblog. front page of the weblog.
- *enclosure* a related resource which is potentially large in size - *enclosure* a related resource which is potentially large in size
and might require special handling, for example an audio or video and might require special handling, for example an audio or video
recording. recording.
@ -397,9 +397,9 @@ class FeedEntry(object):
RSS only supports one link with nothing but a URL. So for the RSS link RSS only supports one link with nothing but a URL. So for the RSS link
element the last link with rel=alternate is used. element the last link with rel=alternate is used.
RSS also supports one enclusure element per entry which is covered by the RSS also supports one enclusure element per entry which is covered by
link element in ATOM feed entries. So for the RSS enclusure element the the link element in ATOM feed entries. So for the RSS enclusure element
last link with rel=enclosure is used. the last link with rel=enclosure is used.
:param link: Dict or list of dicts with data. :param link: Dict or list of dicts with data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
@ -407,59 +407,59 @@ class FeedEntry(object):
''' '''
if link is None and kwargs: if link is None and kwargs:
link = kwargs link = kwargs
if not link is None: if link is not None:
if replace or self.__atom_link is None: if replace or self.__atom_link is None:
self.__atom_link = [] self.__atom_link = []
self.__atom_link += ensure_format( link, self.__atom_link += ensure_format(
link,
set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), set(['href', 'rel', 'type', 'hreflang', 'title', 'length']),
set(['href']), set(['href']),
{'rel':['alternate', 'enclosure', 'related', 'self', 'via']}, {'rel': ['alternate', 'enclosure', 'related', 'self', 'via']},
{'rel': 'alternate'} ) {'rel': 'alternate'})
# RSS only needs one URL. We use the first link for RSS: # RSS only needs one URL. We use the first link for RSS:
for l in self.__atom_link: for l in self.__atom_link:
if l.get('rel') == 'alternate': if l.get('rel') == 'alternate':
self.__rss_link = l['href'] self.__rss_link = l['href']
elif l.get('rel') == 'enclosure': elif l.get('rel') == 'enclosure':
self.__rss_enclosure = {'url':l['href']} self.__rss_enclosure = {'url': l['href']}
self.__rss_enclosure['type'] = l.get('type') self.__rss_enclosure['type'] = l.get('type')
self.__rss_enclosure['length'] = l.get('length') or '0' self.__rss_enclosure['length'] = l.get('length') or '0'
# return the set with more information (atom) # return the set with more information (atom)
return self.__atom_link return self.__atom_link
def summary(self, summary=None): def summary(self, summary=None):
'''Get or set the summary element of an entry which conveys a short '''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 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 element and should be provided if there either is no content provided
the entry, or that content is not inline (i.e., contains a src for the entry, or that content is not inline (i.e., contains a src
attribute), or if the content is encoded in base64. attribute), or if the content is encoded in base64. This method will
This method will also set the rss:description field if it wasn't also set the rss:description field if it wasn't previously set or
previously set or contains the old value of summary. contains the old value of summary.
:param summary: Summary of the entries contents. :param summary: Summary of the entries contents.
:returns: Summary of the entries contents. :returns: Summary of the entries contents.
''' '''
if not summary is None: if summary is not None:
# Replace the RSS description with the summary if it was the summary # Replace the RSS description with the summary if it was the
# before. Not if is the description. # summary before. Not if is the description.
if not self.__rss_description or \ if not self.__rss_description or \
self.__rss_description == self.__atom_summary: self.__rss_description == self.__atom_summary:
self.__rss_description = summary self.__rss_description = summary
self.__atom_summary = summary self.__atom_summary = summary
return self.__atom_summary return self.__atom_summary
def description(self, description=None, isSummary=False): def description(self, description=None, isSummary=False):
'''Get or set the description value which is the item synopsis. '''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 Description is an RSS only element. For ATOM feeds it is split in
and content. The isSummary parameter can be used to control which ATOM summary and content. The isSummary parameter can be used to control
value is set when setting description. which ATOM value is set when setting description.
:param description: Description of the entry. :param description: Description of the entry.
:param isSummary: If the description should be used as content or summary. :param isSummary: If the description should be used as content or
summary.
:returns: The entries description. :returns: The entries description.
''' '''
if not description is None: if description is not None:
self.__rss_description = description self.__rss_description = description
if isSummary: if isSummary:
self.__atom_summary = description self.__atom_summary = description
@ -467,7 +467,6 @@ class FeedEntry(object):
self.__atom_content = description self.__atom_content = description
return self.__rss_description return self.__rss_description
def category(self, category=None, replace=False, **kwargs): def category(self, category=None, replace=False, **kwargs):
'''Get or set categories that the entry belongs to. '''Get or set categories that the entry belongs to.
@ -481,8 +480,8 @@ class FeedEntry(object):
- *scheme* identifies the categorization scheme via a URI. - *scheme* identifies the categorization scheme via a URI.
- *label* provides a human-readable label for display - *label* provides a human-readable label for display
If a label is present it is used for the RSS feeds. Otherwise the term is If a label is present it is used for the RSS feeds. Otherwise the term
used. The scheme is used for the domain attribute in RSS. is used. The scheme is used for the domain attribute in RSS.
:param category: Dict or list of dicts with data. :param category: Dict or list of dicts with data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
@ -490,26 +489,25 @@ class FeedEntry(object):
''' '''
if category is None and kwargs: if category is None and kwargs:
category = kwargs category = kwargs
if not category is None: if category is not None:
if replace or self.__atom_category is None: if replace or self.__atom_category is None:
self.__atom_category = [] self.__atom_category = []
self.__atom_category += ensure_format( self.__atom_category += ensure_format(
category, category,
set(['term', 'scheme', 'label']), set(['term', 'scheme', 'label']),
set(['term']) ) set(['term']))
# Map the ATOM categories to RSS categories. Use the atom:label as # Map the ATOM categories to RSS categories. Use the atom:label as
# name or if not present the atom:term. The atom:scheme is the # name or if not present the atom:term. The atom:scheme is the
# rss:domain. # rss:domain.
self.__rss_category = [] self.__rss_category = []
for cat in self.__atom_category: for cat in self.__atom_category:
rss_cat = {} rss_cat = {}
rss_cat['value'] = cat['label'] if cat.get('label') else cat['term'] rss_cat['value'] = cat.get('label', cat['term'])
if cat.get('scheme'): if cat.get('scheme'):
rss_cat['domain'] = cat['scheme'] rss_cat['domain'] = cat['scheme']
self.__rss_category.append( rss_cat ) self.__rss_category.append(rss_cat)
return self.__atom_category return self.__atom_category
def contributor(self, contributor=None, replace=False, **kwargs): def contributor(self, contributor=None, replace=False, **kwargs):
'''Get or set the contributor data of the feed. This is an ATOM only '''Get or set the contributor data of the feed. This is an ATOM only
value. value.
@ -524,32 +522,32 @@ class FeedEntry(object):
- *uri* contains a home page for the person. - *uri* contains a home page for the person.
- *email* contains an email address for the person. - *email* contains an email address for the person.
:param contributor: Dictionary or list of dictionaries with contributor data. :param contributor: Dictionary or list of dictionaries with contributor
data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of contributors as dictionaries. :returns: List of contributors as dictionaries.
''' '''
if contributor is None and kwargs: if contributor is None and kwargs:
contributor = kwargs contributor = kwargs
if not contributor is None: if contributor is not None:
if replace or self.__atom_contributor is None: if replace or self.__atom_contributor is None:
self.__atom_contributor = [] self.__atom_contributor = []
self.__atom_contributor += ensure_format( contributor, self.__atom_contributor += ensure_format(
set(['name', 'email', 'uri']), set(['name'])) contributor, set(['name', 'email', 'uri']), set(['name']))
return self.__atom_contributor return self.__atom_contributor
def published(self, published=None): def published(self, published=None):
'''Set or get the published value which contains the time of the initial '''Set or get the published value which contains the time of the initial
creation or first availability of the entry. creation or first availability of the entry.
The value can either be a string which will automatically be parsed or a The value can either be a string which will automatically be parsed or
datetime.datetime object. In any case it is necessary that the value a datetime.datetime object. In any case it is necessary that the value
include timezone information. include timezone information.
:param published: The creation date. :param published: The creation date.
:returns: Creation date as datetime.datetime :returns: Creation date as datetime.datetime
''' '''
if not published is None: if published is not None:
if isinstance(published, string_types): if isinstance(published, string_types):
published = dateutil.parser.parse(published) published = dateutil.parser.parse(published)
if not isinstance(published, datetime): if not isinstance(published, datetime):
@ -561,71 +559,65 @@ class FeedEntry(object):
return self.__atom_published return self.__atom_published
def pubdate(self, pubDate=None): def pubdate(self, pubDate=None):
'''Get or set the pubDate of the entry which indicates when the entry was '''Get or set the pubDate of the entry which indicates when the entry
published. This method is just another name for the published(...) was published. This method is just another name for the published(...)
method. method.
''' '''
return self.published(pubDate) return self.published(pubDate)
def rights(self, rights=None): def rights(self, rights=None):
'''Get or set the rights value of the entry which conveys information '''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 about rights, e.g. copyrights, held in and over the entry. This ATOM
will also set rss:copyright. value will also set rss:copyright.
:param rights: Rights information of the feed. :param rights: Rights information of the feed.
:returns: Rights information of the feed. :returns: Rights information of the feed.
''' '''
if not rights is None: if rights is not None:
self.__atom_rights = rights self.__atom_rights = rights
return self.__atom_rights return self.__atom_rights
def comments(self, comments=None): def comments(self, comments=None):
'''Get or set the the value of comments which is the url of the comments '''Get or set the the value of comments which is the url of the
page for the item. This is a RSS only value. comments page for the item. This is a RSS only value.
:param comments: URL to the comments page. :param comments: URL to the comments page.
:returns: URL to the comments page. :returns: URL to the comments page.
''' '''
if not comments is None: if comments is not None:
self.__rss_comments = comments self.__rss_comments = comments
return self.__rss_comments return self.__rss_comments
def enclosure(self, url=None, length=None, type=None): def enclosure(self, url=None, length=None, type=None):
'''Get or set the value of enclosure which describes a media object that '''Get or set the value of enclosure which describes a media object
is attached to the item. This is a RSS only value which is represented by that is attached to the item. This is a RSS only value which is
link(rel=enclosure) in ATOM. ATOM feeds can furthermore contain several represented by link(rel=enclosure) in ATOM. ATOM feeds can furthermore
enclosures while RSS may contain only one. That is why this method, if contain several enclosures while RSS may contain only one. That is why
repeatedly called, will add more than one enclosures to the feed. this method, if repeatedly called, will add more than one enclosures to
However, only the last one is used for RSS. the feed. However, only the last one is used for RSS.
:param url: URL of the media object. :param url: URL of the media object.
:param length: Size of the media in bytes. :param length: Size of the media in bytes.
:param type: Mimetype of the linked media. :param type: Mimetype of the linked media.
:returns: Data of the enclosure element. :returns: Data of the enclosure element.
''' '''
if not url is None: if url is not None:
self.link( href=url, rel='enclosure', type=type, length=length ) self.link(href=url, rel='enclosure', type=type, length=length)
return self.__rss_enclosure return self.__rss_enclosure
def ttl(self, ttl=None): def ttl(self, ttl=None):
'''Get or set the ttl value. It is an RSS only element. ttl stands for '''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 time to live. It's a number of minutes that indicates how long a
can be cached before refreshing from the source. channel can be cached before refreshing from the source.
:param ttl: Integer value representing the time to live. :param ttl: Integer value representing the time to live.
:returns: Time to live of of the entry. :returns: Time to live of of the entry.
''' '''
if not ttl is None: if ttl is not None:
self.__rss_ttl = int(ttl) self.__rss_ttl = int(ttl)
return self.__rss_ttl return self.__rss_ttl
def load_extension(self, name, atom=True, rss=True): def load_extension(self, name, atom=True, rss=True):
'''Load a specific extension by name. '''Load a specific extension by name.
@ -651,7 +643,6 @@ class FeedEntry(object):
ext = getattr(extmod, extname) ext = getattr(extmod, extname)
self.register_extension(name, ext, atom, rss) self.register_extension(name, ext, atom, rss)
def register_extension(self, namespace, extension_class_entry=None, def register_extension(self, namespace, extension_class_entry=None,
atom=True, rss=True): atom=True, rss=True):
'''Register a specific extension by classes to a namespace. '''Register a specific extension by classes to a namespace.
@ -675,8 +666,8 @@ class FeedEntry(object):
# `load_extension` registry # `load_extension` registry
self.__extensions[namespace] = { self.__extensions[namespace] = {
'inst':extinst, 'inst': extinst,
'extension_class_entry': extension_class_entry, 'extension_class_entry': extension_class_entry,
'atom':atom, 'atom': atom,
'rss':rss 'rss': rss
} }

View file

@ -16,7 +16,8 @@ class BaseExtension(object):
'''Basic FeedGenerator extension. '''Basic FeedGenerator extension.
''' '''
def extend_ns(self): def extend_ns(self):
'''Returns a dict that will be used in the namespace map for the feed.''' '''Returns a dict that will be used in the namespace map for the feed.
'''
return dict() return dict()
def extend_rss(self, feed): def extend_rss(self, feed):
@ -27,7 +28,6 @@ class BaseExtension(object):
''' '''
return feed return feed
def extend_atom(self, feed): def extend_atom(self, feed):
'''Extend an ATOM feed xml structure containing all previously set '''Extend an ATOM feed xml structure containing all previously set
fields. fields.

View file

@ -8,20 +8,19 @@
Descriptions partly taken from Descriptions partly taken from
http://dublincore.org/documents/dcmi-terms/#elements-coverage http://dublincore.org/documents/dcmi-terms/#elements-coverage
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013-2016, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
from lxml import etree from lxml import etree
from feedgen.ext.base import BaseExtension, BaseEntryExtension from feedgen.ext.base import BaseExtension
class DcBaseExtension(BaseExtension): class DcBaseExtension(BaseExtension):
'''Dublin Core Elements extension for podcasts. '''Dublin Core Elements extension for podcasts.
''' '''
def __init__(self): def __init__(self):
# http://dublincore.org/documents/usageguide/elements.shtml # http://dublincore.org/documents/usageguide/elements.shtml
# http://dublincore.org/documents/dces/ # http://dublincore.org/documents/dces/
@ -43,7 +42,7 @@ class DcBaseExtension(BaseExtension):
self._dcelem_type = None self._dcelem_type = None
def extend_ns(self): def extend_ns(self):
return {'dc' : 'http://purl.org/dc/elements/1.1/'} return {'dc': 'http://purl.org/dc/elements/1.1/'}
def _extend_xml(self, xml_elem): def _extend_xml(self, xml_elem):
'''Extend xml_elem with set DC fields. '''Extend xml_elem with set DC fields.
@ -52,15 +51,16 @@ class DcBaseExtension(BaseExtension):
''' '''
DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/' DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/'
for elem in ['contributor', 'coverage', 'creator', 'date', 'description', for elem in ['contributor', 'coverage', 'creator', 'date',
'language', 'publisher', 'relation', 'rights', 'source', 'subject', 'description', 'language', 'publisher', 'relation',
'title', 'type', 'format', 'identifier']: 'rights', 'source', 'subject', 'title', 'type', 'format',
'identifier']:
if hasattr(self, '_dcelem_%s' % elem): if hasattr(self, '_dcelem_%s' % elem):
for val in getattr(self, '_dcelem_%s' % elem) or []: for val in getattr(self, '_dcelem_%s' % elem) or []:
node = etree.SubElement(xml_elem, '{%s}%s' % (DCELEMENTS_NS, elem)) node = etree.SubElement(xml_elem,
'{%s}%s' % (DCELEMENTS_NS, elem))
node.text = val node.text = val
def extend_atom(self, atom_feed): def extend_atom(self, atom_feed):
'''Extend an Atom feed with the set DC fields. '''Extend an Atom feed with the set DC fields.
@ -72,8 +72,6 @@ class DcBaseExtension(BaseExtension):
return atom_feed return atom_feed
def extend_rss(self, rss_feed): def extend_rss(self, rss_feed):
'''Extend a RSS feed with the set DC fields. '''Extend a RSS feed with the set DC fields.
@ -85,7 +83,6 @@ class DcBaseExtension(BaseExtension):
return rss_feed return rss_feed
def dc_contributor(self, contributor=None, replace=False): def dc_contributor(self, contributor=None, replace=False):
'''Get or set the dc:contributor which is an entity responsible for '''Get or set the dc:contributor which is an entity responsible for
making contributions to the resource. making contributions to the resource.
@ -97,7 +94,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set contributors (deault: False). :param replace: Replace alredy set contributors (deault: False).
:returns: List of contributors. :returns: List of contributors.
''' '''
if not contributor is None: if contributor is not None:
if not isinstance(contributor, list): if not isinstance(contributor, list):
contributor = [contributor] contributor = [contributor]
if replace or not self._dcelem_contributor: if replace or not self._dcelem_contributor:
@ -105,28 +102,28 @@ class DcBaseExtension(BaseExtension):
self._dcelem_contributor += contributor self._dcelem_contributor += contributor
return self._dcelem_contributor return self._dcelem_contributor
def dc_coverage(self, coverage=None, replace=True): def dc_coverage(self, coverage=None, replace=True):
'''Get or set the dc:coverage which indicated the spatial or temporal '''Get or set the dc:coverage which indicated the spatial or temporal
topic of the resource, the spatial applicability of the resource, or the topic of the resource, the spatial applicability of the resource, or
jurisdiction under which the resource is relevant. the jurisdiction under which the resource is relevant.
Spatial topic and spatial applicability may be a named place or a Spatial topic and spatial applicability may be a named place or a
location specified by its geographic coordinates. Temporal topic may be a location specified by its geographic coordinates. Temporal topic may be
named period, date, or date range. A jurisdiction may be a named a named period, date, or date range. A jurisdiction may be a named
administrative entity or a geographic place to which the resource administrative entity or a geographic place to which the resource
applies. Recommended best practice is to use a controlled vocabulary such applies. Recommended best practice is to use a controlled vocabulary
as the Thesaurus of Geographic Names [TGN]. Where appropriate, named such as the Thesaurus of Geographic Names [TGN]. Where appropriate,
places or time periods can be used in preference to numeric identifiers named places or time periods can be used in preference to numeric
such as sets of coordinates or date ranges. identifiers such as sets of coordinates or date ranges.
References: [TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html References:
[TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html
:param coverage: Coverage of the feed. :param coverage: Coverage of the feed.
:param replace: Replace already set coverage (default: True). :param replace: Replace already set coverage (default: True).
:returns: Coverage of the feed. :returns: Coverage of the feed.
''' '''
if not coverage is None: if coverage is not None:
if not isinstance(coverage, list): if not isinstance(coverage, list):
coverage = [coverage] coverage = [coverage]
if replace or not self._dcelem_coverage: if replace or not self._dcelem_coverage:
@ -134,10 +131,9 @@ class DcBaseExtension(BaseExtension):
self._dcelem_coverage = coverage self._dcelem_coverage = coverage
return self._dcelem_coverage return self._dcelem_coverage
def dc_creator(self, creator=None, replace=False): def dc_creator(self, creator=None, replace=False):
'''Get or set the dc:creator which is an entity primarily responsible for '''Get or set the dc:creator which is an entity primarily responsible
making the resource. for making the resource.
For more information see: For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-creator http://dublincore.org/documents/dcmi-terms/#elements-creator
@ -146,7 +142,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set creators (deault: False). :param replace: Replace alredy set creators (deault: False).
:returns: List of creators. :returns: List of creators.
''' '''
if not creator is None: if creator is not None:
if not isinstance(creator, list): if not isinstance(creator, list):
creator = [creator] creator = [creator]
if replace or not self._dcelem_creator: if replace or not self._dcelem_creator:
@ -154,7 +150,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_creator += creator self._dcelem_creator += creator
return self._dcelem_creator return self._dcelem_creator
def dc_date(self, date=None, replace=True): def dc_date(self, date=None, replace=True):
'''Get or set the dc:date which describes a point or period of time '''Get or set the dc:date which describes a point or period of time
associated with an event in the lifecycle of the resource. associated with an event in the lifecycle of the resource.
@ -166,7 +161,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set dates (deault: True). :param replace: Replace alredy set dates (deault: True).
:returns: List of dates. :returns: List of dates.
''' '''
if not date is None: if date is not None:
if not isinstance(date, list): if not isinstance(date, list):
date = [date] date = [date]
if replace or not self._dcelem_date: if replace or not self._dcelem_date:
@ -174,7 +169,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_date += date self._dcelem_date += date
return self._dcelem_date return self._dcelem_date
def dc_description(self, description=None, replace=True): def dc_description(self, description=None, replace=True):
'''Get or set the dc:description which is an account of the resource. '''Get or set the dc:description which is an account of the resource.
@ -185,7 +179,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set descriptions (deault: True). :param replace: Replace alredy set descriptions (deault: True).
:returns: List of descriptions. :returns: List of descriptions.
''' '''
if not description is None: if description is not None:
if not isinstance(description, list): if not isinstance(description, list):
description = [description] description = [description]
if replace or not self._dcelem_description: if replace or not self._dcelem_description:
@ -193,7 +187,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_description += description self._dcelem_description += description
return self._dcelem_description return self._dcelem_description
def dc_format(self, format=None, replace=True): def dc_format(self, format=None, replace=True):
'''Get or set the dc:format which describes the file format, physical '''Get or set the dc:format which describes the file format, physical
medium, or dimensions of the resource. medium, or dimensions of the resource.
@ -205,7 +198,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set format (deault: True). :param replace: Replace alredy set format (deault: True).
:returns: Format of the resource. :returns: Format of the resource.
''' '''
if not format is None: if format is not None:
if not isinstance(format, list): if not isinstance(format, list):
format = [format] format = [format]
if replace or not self._dcelem_format: if replace or not self._dcelem_format:
@ -213,10 +206,9 @@ class DcBaseExtension(BaseExtension):
self._dcelem_format += format self._dcelem_format += format
return self._dcelem_format return self._dcelem_format
def dc_identifier(self, identifier=None, replace=True): def dc_identifier(self, identifier=None, replace=True):
'''Get or set the dc:identifier which should be an unambiguous reference '''Get or set the dc:identifier which should be an unambiguous
to the resource within a given context. reference to the resource within a given context.
For more inidentifierion see: For more inidentifierion see:
http://dublincore.org/documents/dcmi-terms/#elements-identifier http://dublincore.org/documents/dcmi-terms/#elements-identifier
@ -225,16 +217,16 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set identifier (deault: True). :param replace: Replace alredy set identifier (deault: True).
:returns: Identifiers of the resource. :returns: Identifiers of the resource.
''' '''
if not identifier is None: if identifier is not None:
if not isinstance(identifier, list): if not isinstance(identifier, list):
identifier = [identifier] identifier = [identifier]
if replace or not self._dcelem_identifier: if replace or not self._dcelem_identifier:
self._dcelem_identifier = [] self._dcelem_identifier = []
self._dcelem_identifier += identifier self._dcelem_identifier += identifier
def dc_language(self, language=None, replace=True): def dc_language(self, language=None, replace=True):
'''Get or set the dc:language which describes a language of the resource. '''Get or set the dc:language which describes a language of the
resource.
For more information see: For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-language http://dublincore.org/documents/dcmi-terms/#elements-language
@ -243,7 +235,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set languages (deault: True). :param replace: Replace alredy set languages (deault: True).
:returns: List of languages. :returns: List of languages.
''' '''
if not language is None: if language is not None:
if not isinstance(language, list): if not isinstance(language, list):
language = [language] language = [language]
if replace or not self._dcelem_language: if replace or not self._dcelem_language:
@ -251,10 +243,9 @@ class DcBaseExtension(BaseExtension):
self._dcelem_language += language self._dcelem_language += language
return self._dcelem_language return self._dcelem_language
def dc_publisher(self, publisher=None, replace=False): def dc_publisher(self, publisher=None, replace=False):
'''Get or set the dc:publisher which is an entity responsible for making '''Get or set the dc:publisher which is an entity responsible for
the resource available. making the resource available.
For more information see: For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-publisher http://dublincore.org/documents/dcmi-terms/#elements-publisher
@ -263,7 +254,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set publishers (deault: False). :param replace: Replace alredy set publishers (deault: False).
:returns: List of publishers. :returns: List of publishers.
''' '''
if not publisher is None: if publisher is not None:
if not isinstance(publisher, list): if not isinstance(publisher, list):
publisher = [publisher] publisher = [publisher]
if replace or not self._dcelem_publisher: if replace or not self._dcelem_publisher:
@ -271,7 +262,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_publisher += publisher self._dcelem_publisher += publisher
return self._dcelem_publisher return self._dcelem_publisher
def dc_relation(self, relation=None, replace=False): def dc_relation(self, relation=None, replace=False):
'''Get or set the dc:relation which describes a related ressource. '''Get or set the dc:relation which describes a related ressource.
@ -282,7 +272,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set relations (deault: False). :param replace: Replace alredy set relations (deault: False).
:returns: List of relations. :returns: List of relations.
''' '''
if not relation is None: if relation is not None:
if not isinstance(relation, list): if not isinstance(relation, list):
relation = [relation] relation = [relation]
if replace or not self._dcelem_relation: if replace or not self._dcelem_relation:
@ -290,7 +280,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_relation += relation self._dcelem_relation += relation
return self._dcelem_relation return self._dcelem_relation
def dc_rights(self, rights=None, replace=False): def dc_rights(self, rights=None, replace=False):
'''Get or set the dc:rights which may contain information about rights '''Get or set the dc:rights which may contain information about rights
held in and over the resource. held in and over the resource.
@ -302,7 +291,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set rightss (deault: False). :param replace: Replace alredy set rightss (deault: False).
:returns: List of rights information. :returns: List of rights information.
''' '''
if not rights is None: if rights is not None:
if not isinstance(rights, list): if not isinstance(rights, list):
rights = [rights] rights = [rights]
if replace or not self._dcelem_rights: if replace or not self._dcelem_rights:
@ -310,15 +299,14 @@ class DcBaseExtension(BaseExtension):
self._dcelem_rights += rights self._dcelem_rights += rights
return self._dcelem_rights return self._dcelem_rights
def dc_source(self, source=None, replace=False): def dc_source(self, source=None, replace=False):
'''Get or set the dc:source which is a related resource from which the '''Get or set the dc:source which is a related resource from which the
described resource is derived. described resource is derived.
The described resource may be derived from the related resource in whole The described resource may be derived from the related resource in
or in part. Recommended best practice is to identify the related resource whole or in part. Recommended best practice is to identify the related
by means of a string conforming to a formal identification system. resource by means of a string conforming to a formal identification
system.
For more information see: For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-source http://dublincore.org/documents/dcmi-terms/#elements-source
@ -327,7 +315,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set sources (deault: False). :param replace: Replace alredy set sources (deault: False).
:returns: List of sources. :returns: List of sources.
''' '''
if not source is None: if source is not None:
if not isinstance(source, list): if not isinstance(source, list):
source = [source] source = [source]
if replace or not self._dcelem_source: if replace or not self._dcelem_source:
@ -335,7 +323,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_source += source self._dcelem_source += source
return self._dcelem_source return self._dcelem_source
def dc_subject(self, subject=None, replace=False): def dc_subject(self, subject=None, replace=False):
'''Get or set the dc:subject which describes the topic of the resource. '''Get or set the dc:subject which describes the topic of the resource.
@ -346,7 +333,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set subjects (deault: False). :param replace: Replace alredy set subjects (deault: False).
:returns: List of subjects. :returns: List of subjects.
''' '''
if not subject is None: if subject is not None:
if not isinstance(subject, list): if not isinstance(subject, list):
subject = [subject] subject = [subject]
if replace or not self._dcelem_subject: if replace or not self._dcelem_subject:
@ -354,7 +341,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_subject += subject self._dcelem_subject += subject
return self._dcelem_subject return self._dcelem_subject
def dc_title(self, title=None, replace=True): def dc_title(self, title=None, replace=True):
'''Get or set the dc:title which is a name given to the resource. '''Get or set the dc:title which is a name given to the resource.
@ -365,7 +351,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set titles (deault: False). :param replace: Replace alredy set titles (deault: False).
:returns: List of titles. :returns: List of titles.
''' '''
if not title is None: if title is not None:
if not isinstance(title, list): if not isinstance(title, list):
title = [title] title = [title]
if replace or not self._dcelem_title: if replace or not self._dcelem_title:
@ -373,7 +359,6 @@ class DcBaseExtension(BaseExtension):
self._dcelem_title += title self._dcelem_title += title
return self._dcelem_title return self._dcelem_title
def dc_type(self, type=None, replace=False): def dc_type(self, type=None, replace=False):
'''Get or set the dc:type which describes the nature or genre of the '''Get or set the dc:type which describes the nature or genre of the
resource. resource.
@ -385,7 +370,7 @@ class DcBaseExtension(BaseExtension):
:param replace: Replace alredy set types (deault: False). :param replace: Replace alredy set types (deault: False).
:returns: List of types. :returns: List of types.
''' '''
if not type is None: if type is not None:
if not isinstance(type, list): if not isinstance(type, list):
type = [type] type = [type]
if replace or not self._dcelem_type: if replace or not self._dcelem_type:
@ -393,10 +378,12 @@ class DcBaseExtension(BaseExtension):
self._dcelem_type += type self._dcelem_type += type
return self._dcelem_type return self._dcelem_type
class DcExtension(DcBaseExtension): class DcExtension(DcBaseExtension):
'''Dublin Core Elements extension for podcasts. '''Dublin Core Elements extension for podcasts.
''' '''
class DcEntryExtension(DcBaseExtension): class DcEntryExtension(DcBaseExtension):
'''Dublin Core Elements extension for podcasts. '''Dublin Core Elements extension for podcasts.
''' '''

View file

@ -20,9 +20,8 @@ class PodcastExtension(BaseExtension):
'''FeedGenerator extension for podcasts. '''FeedGenerator extension for podcasts.
''' '''
def __init__(self): def __init__(self):
## ITunes tags # ITunes tags
# http://www.apple.com/itunes/podcasts/specs.html#rss # http://www.apple.com/itunes/podcasts/specs.html#rss
self.__itunes_author = None self.__itunes_author = None
self.__itunes_block = None self.__itunes_block = None
@ -35,10 +34,8 @@ class PodcastExtension(BaseExtension):
self.__itunes_subtitle = None self.__itunes_subtitle = None
self.__itunes_summary = None self.__itunes_summary = None
def extend_ns(self): def extend_ns(self):
return {'itunes' : 'http://www.itunes.com/dtds/podcast-1.0.dtd'} return {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
def extend_rss(self, rss_feed): def extend_rss(self, rss_feed):
'''Extend an RSS feed root with set itunes fields. '''Extend an RSS feed root with set itunes fields.
@ -52,20 +49,23 @@ class PodcastExtension(BaseExtension):
author = etree.SubElement(channel, '{%s}author' % ITUNES_NS) author = etree.SubElement(channel, '{%s}author' % ITUNES_NS)
author.text = self.__itunes_author author.text = self.__itunes_author
if not self.__itunes_block is None: if self.__itunes_block is not None:
block = etree.SubElement(channel, '{%s}block' % ITUNES_NS) block = etree.SubElement(channel, '{%s}block' % ITUNES_NS)
block.text = 'yes' if self.__itunes_block else 'no' block.text = 'yes' if self.__itunes_block else 'no'
for c in self.__itunes_category or []: for c in self.__itunes_category or []:
if not c.get('cat'): if not c.get('cat'):
continue continue
category = channel.find('{%s}category[@text="%s"]' % (ITUNES_NS,c.get('cat'))) category = channel.find(
if category == None: '{%s}category[@text="%s"]' % (ITUNES_NS, c.get('cat')))
category = etree.SubElement(channel, '{%s}category' % ITUNES_NS) if category is None:
category = etree.SubElement(channel,
'{%s}category' % ITUNES_NS)
category.attrib['text'] = c.get('cat') category.attrib['text'] = c.get('cat')
if c.get('sub'): if c.get('sub'):
subcategory = etree.SubElement(category, '{%s}category' % ITUNES_NS) subcategory = etree.SubElement(category,
'{%s}category' % ITUNES_NS)
subcategory.attrib['text'] = c.get('sub') subcategory.attrib['text'] = c.get('sub')
if self.__itunes_image: if self.__itunes_image:
@ -81,7 +81,8 @@ class PodcastExtension(BaseExtension):
complete.text = self.__itunes_complete complete.text = self.__itunes_complete
if self.__itunes_new_feed_url: if self.__itunes_new_feed_url:
new_feed_url = etree.SubElement(channel, '{%s}new-feed-url' % ITUNES_NS) new_feed_url = etree.SubElement(channel,
'{%s}new-feed-url' % ITUNES_NS)
new_feed_url.text = self.__itunes_new_feed_url new_feed_url.text = self.__itunes_new_feed_url
if self.__itunes_owner: if self.__itunes_owner:
@ -101,29 +102,27 @@ class PodcastExtension(BaseExtension):
return rss_feed return rss_feed
def itunes_author(self, itunes_author=None): def itunes_author(self, itunes_author=None):
'''Get or set the itunes:author. The content of this tag is shown in the '''Get or set the itunes:author. The content of this tag is shown in
Artist column in iTunes. If the tag is not present, iTunes uses the the Artist column in iTunes. If the tag is not present, iTunes uses the
contents of the <author> tag. If <itunes:author> is not present at the contents of the <author> tag. If <itunes:author> is not present at the
feed level, iTunes will use the contents of <managingEditor>. feed level, iTunes will use the contents of <managingEditor>.
:param itunes_author: The author of the podcast. :param itunes_author: The author of the podcast.
:returns: The author of the podcast. :returns: The author of the podcast.
''' '''
if not itunes_author is None: if itunes_author is not None:
self.__itunes_author = itunes_author self.__itunes_author = itunes_author
return self.__itunes_author return self.__itunes_author
def itunes_block(self, itunes_block=None): def itunes_block(self, itunes_block=None):
'''Get or set the ITunes block attribute. Use this to prevent the entire '''Get or set the ITunes block attribute. Use this to prevent the
podcast from appearing in the iTunes podcast directory. entire podcast from appearing in the iTunes podcast directory.
:param itunes_block: Block the podcast. :param itunes_block: Block the podcast.
:returns: If the podcast is blocked. :returns: If the podcast is blocked.
''' '''
if not itunes_block is None: if itunes_block is not None:
self.__itunes_block = itunes_block self.__itunes_block = itunes_block
return self.__itunes_block return self.__itunes_block
@ -167,36 +166,36 @@ class PodcastExtension(BaseExtension):
--- ---
**Important note about deprecated parameter syntax:** Old version of the **Important note about deprecated parameter syntax:** Old version of
feedgen did only support one category plus one subcategory which would be the feedgen did only support one category plus one subcategory which
passed to this ducntion as first two parameters. For compatibility would be passed to this ducntion as first two parameters. For
reasons, this still works but should not be used any may be removed at compatibility reasons, this still works but should not be used any may
any time. be removed at any time.
''' '''
# Ensure old API still works for now. Note that the API is deprecated and # Ensure old API still works for now. Note that the API is deprecated
# this fallback may be removed at any time. # and this fallback may be removed at any time.
if isinstance(itunes_category, string_types): if isinstance(itunes_category, string_types):
itunes_category = {'cat':itunes_category} itunes_category = {'cat': itunes_category}
if replace: if replace:
itunes_category['sub'] = replace itunes_category['sub'] = replace
replace=True replace = True
if itunes_category is None and kwargs: if itunes_category is None and kwargs:
itunes_category = kwargs itunes_category = kwargs
if not itunes_category is None: if itunes_category is not None:
if replace or self.__itunes_category is None: if replace or self.__itunes_category is None:
self.__itunes_category = [] self.__itunes_category = []
self.__itunes_category += ensure_format( itunes_category, self.__itunes_category += ensure_format(itunes_category,
set(['cat', 'sub']), set(['cat'])) set(['cat', 'sub']),
set(['cat']))
return self.__itunes_category return self.__itunes_category
def itunes_image(self, itunes_image=None): def itunes_image(self, itunes_image=None):
'''Get or set the image for the podcast. This tag specifies the artwork '''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 for your podcast. Put the URL to the image in the href attribute.
prefers square .jpg images that are at least 1400x1400 pixels, which is iTunes prefers square .jpg images that are at least 1400x1400 pixels,
different from what is specified for the standard RSS image tag. In order which is different from what is specified for the standard RSS image
for a podcast to be eligible for an iTunes Store feature, the tag. In order for a podcast to be eligible for an iTunes Store feature,
accompanying image must be at least 1400x1400 pixels. the accompanying image must be at least 1400x1400 pixels.
iTunes supports images in JPEG and PNG formats with an RGB color space 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 (CMYK is not supported). The URL must end in ".jpg" or ".png". If the
@ -204,43 +203,43 @@ class PodcastExtension(BaseExtension):
RSS image tag. RSS image tag.
If you change your podcasts image, also change the files name. iTunes If you change your podcasts image, also change the files name. iTunes
may not change the image if it checks your feed and the image URL is the may not change the image if it checks your feed and the image URL is
same. The server hosting your cover art image must allow HTTP head 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. requests for iTS to be able to automatically update your cover art.
:param itunes_image: Image of the podcast. :param itunes_image: Image of the podcast.
:returns: Image of the podcast. :returns: Image of the podcast.
''' '''
if not itunes_image is None: if itunes_image is not None:
if not ( itunes_image.endswith('.jpg') or itunes_image.endswith('.png') ): if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
ValueError('Image file must be png or jpg')
self.__itunes_image = itunes_image self.__itunes_image = itunes_image
else:
ValueError('Image file must be png or jpg')
return self.__itunes_image return self.__itunes_image
def itunes_explicit(self, itunes_explicit=None): def itunes_explicit(self, itunes_explicit=None):
'''Get or the the itunes:explicit value of the podcast. This tag should '''Get or the the itunes:explicit value of the podcast. This tag should
be used to indicate whether your podcast contains explicit material. The be used to indicate whether your podcast contains explicit material.
three values for this tag are "yes", "no", and "clean". The three values for this tag are "yes", "no", and "clean".
If you populate this tag with "yes", an "explicit" parental advisory If you populate this tag with "yes", an "explicit" parental advisory
graphic will appear next to your podcast artwork on the iTunes Store and graphic will appear next to your podcast artwork on the iTunes Store
in the Name column in iTunes. If the value is "clean", the parental 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 advisory type is considered Clean, meaning that no explicit language or
adult content is included anywhere in the episodes, and a "clean" graphic adult content is included anywhere in the episodes, and a "clean"
will appear. If the explicit tag is present and has any other value graphic will appear. If the explicit tag is present and has any other
(e.g., "no"), you see no indicator blank is the default advisory type. value (e.g., "no"), you see no indicator blank is the default
advisory type.
:param itunes_explicit: If the podcast contains explicit material. :param itunes_explicit: If the podcast contains explicit material.
:returns: If the podcast contains explicit material. :returns: If the podcast contains explicit material.
''' '''
if not itunes_explicit is None: if itunes_explicit is not None:
if not itunes_explicit in ('', 'yes', 'no', 'clean'): if itunes_explicit not in ('', 'yes', 'no', 'clean'):
raise ValueError('Invalid value for explicit tag') raise ValueError('Invalid value for explicit tag')
self.__itunes_explicit = itunes_explicit self.__itunes_explicit = itunes_explicit
return self.__itunes_explicit return self.__itunes_explicit
def itunes_complete(self, itunes_complete=None): def itunes_complete(self, itunes_complete=None):
'''Get or set the itunes:complete value of the podcast. This tag can be '''Get or set the itunes:complete value of the podcast. This tag can be
used to indicate the completion of a podcast. used to indicate the completion of a podcast.
@ -253,33 +252,31 @@ class PodcastExtension(BaseExtension):
:param itunes_complete: If the podcast is complete. :param itunes_complete: If the podcast is complete.
:returns: If the podcast is complete. :returns: If the podcast is complete.
''' '''
if not itunes_complete is None: if itunes_complete is not None:
if not itunes_complete in ('yes', 'no', '', True, False): if itunes_complete not in ('yes', 'no', '', True, False):
raise ValueError('Invalid value for complete tag') raise ValueError('Invalid value for complete tag')
if itunes_complete == True: if itunes_complete is True:
itunes_complete = 'yes' itunes_complete = 'yes'
if itunes_complete == False: if itunes_complete is False:
itunes_complete = 'no' itunes_complete = 'no'
self.__itunes_complete = itunes_complete self.__itunes_complete = itunes_complete
return self.__itunes_complete return self.__itunes_complete
def itunes_new_feed_url(self, itunes_new_feed_url=None): 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 '''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 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 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 for 48 hours before retiring it. At that point, iTunes will have
the directory with the new feed URL. updated the directory with the new feed URL.
:param itunes_new_feed_url: New feed URL. :param itunes_new_feed_url: New feed URL.
:returns: New feed URL. :returns: New feed URL.
''' '''
if not itunes_new_feed_url is None: if itunes_new_feed_url is not None:
self.__itunes_new_feed_url = itunes_new_feed_url self.__itunes_new_feed_url = itunes_new_feed_url
return self.__itunes_new_feed_url return self.__itunes_new_feed_url
def itunes_owner(self, name=None, email=None): def itunes_owner(self, name=None, email=None):
'''Get or set the itunes:owner of the podcast. This tag contains '''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 information that will be used to contact the owner of the podcast for
@ -289,16 +286,15 @@ class PodcastExtension(BaseExtension):
:param itunes_owner: The owner of the feed. :param itunes_owner: The owner of the feed.
:returns: Data of the owner of the feed. :returns: Data of the owner of the feed.
''' '''
if not name is None: if name is not None:
if name and email: if name and email:
self.__itunes_owner = {'name':name, 'email':email} self.__itunes_owner = {'name': name, 'email': email}
elif not name and not email: elif not name and not email:
self.__itunes_owner = None self.__itunes_owner = None
else: else:
raise ValueError('Both name and email have to be set.') raise ValueError('Both name and email have to be set.')
return self.__itunes_owner return self.__itunes_owner
def itunes_subtitle(self, itunes_subtitle=None): def itunes_subtitle(self, itunes_subtitle=None):
'''Get or set the itunes:subtitle value for the podcast. The contents of '''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 this tag are shown in the Description column in iTunes. The subtitle
@ -307,53 +303,57 @@ class PodcastExtension(BaseExtension):
:param itunes_subtitle: Subtitle of the podcast. :param itunes_subtitle: Subtitle of the podcast.
:returns: Subtitle of the podcast. :returns: Subtitle of the podcast.
''' '''
if not itunes_subtitle is None: if itunes_subtitle is not None:
self.__itunes_subtitle = itunes_subtitle self.__itunes_subtitle = itunes_subtitle
return self.__itunes_subtitle return self.__itunes_subtitle
def itunes_summary(self, itunes_summary=None): def itunes_summary(self, itunes_summary=None):
'''Get or set the itunes:summary value for the podcast. The contents of '''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" this tag are shown in a separate window that appears when the "circled
in the Description column is clicked. It also appears on the iTunes page i" in the Description column is clicked. It also appears on the iTunes
for your podcast. This field can be up to 4000 characters. If page for your podcast. This field can be up to 4000 characters. If
<itunes:summary> is not included, the contents of the <description> tag `<itunes:summary>` is not included, the contents of the <description>
are used. tag are used.
:param itunes_summary: Summary of the podcast. :param itunes_summary: Summary of the podcast.
:returns: Summary of the podcast. :returns: Summary of the podcast.
''' '''
if not itunes_summary is None: if itunes_summary is not None:
self.__itunes_summary = itunes_summary self.__itunes_summary = itunes_summary
return self.__itunes_summary return self.__itunes_summary
_itunes_categories = { _itunes_categories = {
'Arts': [ 'Design', 'Fashion & Beauty', 'Food', 'Literature', 'Arts': [
'Performing Arts', 'Visual Arts' ], 'Design', 'Fashion & Beauty', 'Food', 'Literature',
'Business' : [ 'Business News', 'Careers', 'Investing', 'Performing Arts', 'Visual Arts'],
'Management & Marketing', 'Shopping' ], 'Business': [
'Comedy' : [], 'Business News', 'Careers', 'Investing',
'Education' : [ 'Education', 'Education Technology', 'Management & Marketing', 'Shopping'],
'Higher Education', 'K-12', 'Language Courses', 'Training' ], 'Comedy': [],
'Games & Hobbies' : [ 'Automotive', 'Aviation', 'Hobbies', 'Education': [
'Other Games', 'Video Games' ], 'Education', 'Education Technology', 'Higher Education',
'Government & Organizations' : [ 'Local', 'National', 'Non-Profit', 'K-12', 'Language Courses', 'Training'],
'Regional' ], 'Games & Hobbies': [
'Health' : [ 'Alternative Health', 'Fitness & Nutrition', 'Self-Help', 'Automotive', 'Aviation', 'Hobbies', 'Other Games',
'Sexuality' ], 'Video Games'],
'Kids & Family' : [], 'Government & Organizations': [
'Music' : [], 'Local', 'National', 'Non-Profit', 'Regional'],
'News & Politics' : [], 'Health': [
'Religion & Spirituality' : [ 'Buddhism', 'Christianity', 'Hinduism', 'Alternative Health', 'Fitness & Nutrition', 'Self-Help',
'Islam', 'Judaism', 'Other', 'Spirituality' ], 'Sexuality'],
'Science & Medicine' : [ 'Medicine', 'Natural Sciences', 'Kids & Family': [],
'Social Sciences' ], 'Music': [],
'Society & Culture' : [ 'History', 'Personal Journals', 'Philosophy', 'News & Politics': [],
'Places & Travel' ], 'Religion & Spirituality': [
'Sports & Recreation' : [ 'Amateur', 'College & High School', 'Buddhism', 'Christianity', 'Hinduism', 'Islam', 'Judaism',
'Outdoor', 'Professional' ], 'Other', 'Spirituality'],
'Technology' : [ 'Gadgets', 'Tech News', 'Podcasting', 'Science & Medicine': [
'Software How-To' ], 'Medicine', 'Natural Sciences', 'Social Sciences'],
'TV & Film' : [] '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': []}

View file

@ -5,7 +5,7 @@
Extends the feedgen to produce podcasts. Extends the feedgen to produce podcasts.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013-2016, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
@ -18,9 +18,8 @@ class PodcastEntryExtension(BaseEntryExtension):
'''FeedEntry extension for podcasts. '''FeedEntry extension for podcasts.
''' '''
def __init__(self): def __init__(self):
## ITunes tags # ITunes tags
# http://www.apple.com/itunes/podcasts/specs.html#rss # http://www.apple.com/itunes/podcasts/specs.html#rss
self.__itunes_author = None self.__itunes_author = None
self.__itunes_block = None self.__itunes_block = None
@ -32,7 +31,6 @@ class PodcastEntryExtension(BaseEntryExtension):
self.__itunes_subtitle = None self.__itunes_subtitle = None
self.__itunes_summary = None self.__itunes_summary = None
def extend_rss(self, entry): def extend_rss(self, entry):
'''Add additional fields to an RSS item. '''Add additional fields to an RSS item.
@ -44,7 +42,7 @@ class PodcastEntryExtension(BaseEntryExtension):
author = etree.SubElement(entry, '{%s}author' % ITUNES_NS) author = etree.SubElement(entry, '{%s}author' % ITUNES_NS)
author.text = self.__itunes_author author.text = self.__itunes_author
if not self.__itunes_block is None: if self.__itunes_block is not None:
block = etree.SubElement(entry, '{%s}block' % ITUNES_NS) block = etree.SubElement(entry, '{%s}block' % ITUNES_NS)
block.text = 'yes' if self.__itunes_block else 'no' block.text = 'yes' if self.__itunes_block else 'no'
@ -60,11 +58,15 @@ class PodcastEntryExtension(BaseEntryExtension):
explicit = etree.SubElement(entry, '{%s}explicit' % ITUNES_NS) explicit = etree.SubElement(entry, '{%s}explicit' % ITUNES_NS)
explicit.text = self.__itunes_explicit explicit.text = self.__itunes_explicit
if not self.__itunes_is_closed_captioned is None: if self.__itunes_is_closed_captioned is not None:
is_closed_captioned = etree.SubElement(entry, '{%s}isClosedCaptioned' % ITUNES_NS) is_closed_captioned = etree.SubElement(
is_closed_captioned.text = 'yes' if self.__itunes_is_closed_captioned else 'no' entry, '{%s}isClosedCaptioned' % ITUNES_NS)
if self.__itunes_is_closed_captioned:
is_closed_captioned.text = 'yes'
else:
is_closed_captioned.text = 'no'
if not self.__itunes_order is None and self.__itunes_order >= 0: if self.__itunes_order is not None and self.__itunes_order >= 0:
order = etree.SubElement(entry, '{%s}order' % ITUNES_NS) order = etree.SubElement(entry, '{%s}order' % ITUNES_NS)
order.text = str(self.__itunes_order) order.text = str(self.__itunes_order)
@ -77,22 +79,20 @@ class PodcastEntryExtension(BaseEntryExtension):
summary.text = self.__itunes_summary summary.text = self.__itunes_summary
return entry return entry
def itunes_author(self, itunes_author=None): def itunes_author(self, itunes_author=None):
'''Get or set the itunes:author of the podcast episode. The content of '''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 this tag is shown in the Artist column in iTunes. If the tag is not
present, iTunes uses the contents of the <author> tag. If <itunes:author> present, iTunes uses the contents of the <author> tag. If
is not present at the feed level, iTunes will use the contents of <itunes:author> is not present at the feed level, iTunes will use the
<managingEditor>. contents of <managingEditor>.
:param itunes_author: The author of the podcast. :param itunes_author: The author of the podcast.
:returns: The author of the podcast. :returns: The author of the podcast.
''' '''
if not itunes_author is None: if itunes_author is not None:
self.__itunes_author = itunes_author self.__itunes_author = itunes_author
return self.__itunes_author return self.__itunes_author
def itunes_block(self, itunes_block=None): def itunes_block(self, itunes_block=None):
'''Get or set the ITunes block attribute. Use this to prevent episodes '''Get or set the ITunes block attribute. Use this to prevent episodes
from appearing in the iTunes podcast directory. from appearing in the iTunes podcast directory.
@ -100,18 +100,18 @@ class PodcastEntryExtension(BaseEntryExtension):
:param itunes_block: Block podcast episodes. :param itunes_block: Block podcast episodes.
:returns: If the podcast episode is blocked. :returns: If the podcast episode is blocked.
''' '''
if not itunes_block is None: if itunes_block is not None:
self.__itunes_block = itunes_block self.__itunes_block = itunes_block
return self.__itunes_block return self.__itunes_block
def itunes_image(self, itunes_image=None): def itunes_image(self, itunes_image=None):
'''Get or set the image for the podcast episode. This tag specifies the '''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. artwork for your podcast. Put the URL to the image in the href
iTunes prefers square .jpg images that are at least 1400x1400 pixels, attribute. iTunes prefers square .jpg images that are at least
which is different from what is specified for the standard RSS image tag. 1400x1400 pixels, which is different from what is specified for the
In order for a podcast to be eligible for an iTunes Store feature, the standard RSS image tag. In order for a podcast to be eligible for an
accompanying image must be at least 1400x1400 pixels. 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 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 (CMYK is not supported). The URL must end in ".jpg" or ".png". If the
@ -119,35 +119,35 @@ class PodcastEntryExtension(BaseEntryExtension):
RSS image tag. RSS image tag.
If you change your podcasts image, also change the files name. iTunes If you change your podcasts image, also change the files name. iTunes
may not change the image if it checks your feed and the image URL is the may not change the image if it checks your feed and the image URL is
same. The server hosting your cover art image must allow HTTP head 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. requests for iTS to be able to automatically update your cover art.
:param itunes_image: Image of the podcast. :param itunes_image: Image of the podcast.
:returns: Image of the podcast. :returns: Image of the podcast.
''' '''
if not itunes_image is None: if itunes_image is not None:
if not ( itunes_image.endswith('.jpg') or itunes_image.endswith('.png') ): if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
ValueError('Image file must be png or jpg')
self.__itunes_image = itunes_image self.__itunes_image = itunes_image
else:
ValueError('Image file must be png or jpg')
return self.__itunes_image return self.__itunes_image
def itunes_duration(self, itunes_duration=None): def itunes_duration(self, itunes_duration=None):
'''Get or set the duration of the podcast episode. The content of this '''Get or set the duration of the podcast episode. The content of this
tag is shown in the Time column in iTunes. 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, 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), M = minutes, S = seconds). If an integer is provided (no colon
the value is assumed to be in seconds. If one colon is present, the present), the value is assumed to be in seconds. If one colon is
number to the left is assumed to be minutes, and the number to the right present, the number to the left is assumed to be minutes, and the
is assumed to be seconds. If more than two colons are present, the number to the right is assumed to be seconds. If more than two colons
numbers farthest to the right are ignored. are present, the numbers farthest to the right are ignored.
:param itunes_duration: Duration of the podcast episode. :param itunes_duration: Duration of the podcast episode.
:returns: Duration of the podcast episode. :returns: Duration of the podcast episode.
''' '''
if not itunes_duration is None: if itunes_duration is not None:
itunes_duration = str(itunes_duration) itunes_duration = str(itunes_duration)
if len(itunes_duration.split(':')) > 3 or \ if len(itunes_duration.split(':')) > 3 or \
itunes_duration.lstrip('0123456789:') != '': itunes_duration.lstrip('0123456789:') != '':
@ -155,65 +155,67 @@ class PodcastEntryExtension(BaseEntryExtension):
self.__itunes_duration = itunes_duration self.__itunes_duration = itunes_duration
return self.itunes_duration return self.itunes_duration
def itunes_explicit(self, itunes_explicit=None): def itunes_explicit(self, itunes_explicit=None):
'''Get or the the itunes:explicit value of the podcast episode. This tag '''Get or the the itunes:explicit value of the podcast episode. This
should be used to indicate whether your podcast episode contains explicit tag should be used to indicate whether your podcast episode contains
material. The three values for this tag are "yes", "no", and "clean". explicit material. The three values for this tag are "yes", "no", and
"clean".
If you populate this tag with "yes", an "explicit" parental advisory If you populate this tag with "yes", an "explicit" parental advisory
graphic will appear next to your podcast artwork on the iTunes Store and graphic will appear next to your podcast artwork on the iTunes Store
in the Name column in iTunes. If the value is "clean", the parental 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 advisory type is considered Clean, meaning that no explicit language or
adult content is included anywhere in the episodes, and a "clean" graphic adult content is included anywhere in the episodes, and a "clean"
will appear. If the explicit tag is present and has any other value graphic will appear. If the explicit tag is present and has any other
(e.g., "no"), you see no indicator blank is the default advisory type. value (e.g., "no"), you see no indicator blank is the default
advisory type.
:param itunes_explicit: If the podcast episode contains explicit material. :param itunes_explicit: If the podcast episode contains explicit
material.
:returns: If the podcast episode contains explicit material. :returns: If the podcast episode contains explicit material.
''' '''
if not itunes_explicit is None: if itunes_explicit is not None:
if not itunes_explicit in ('', 'yes', 'no', 'clean'): if itunes_explicit not in ('', 'yes', 'no', 'clean'):
raise ValueError('Invalid value for explicit tag') raise ValueError('Invalid value for explicit tag')
self.__itunes_explicit = itunes_explicit self.__itunes_explicit = itunes_explicit
return self.__itunes_explicit return self.__itunes_explicit
def itunes_is_closed_captioned(self, itunes_is_closed_captioned=None): def itunes_is_closed_captioned(self, itunes_is_closed_captioned=None):
'''Get or set the is_closed_captioned value of the podcast episode. This '''Get or set the is_closed_captioned value of the podcast episode.
tag should be used if your podcast includes a video episode with embedded This tag should be used if your podcast includes a video episode with
closed captioning support. The two values for this tag are "yes" and embedded closed captioning support. The two values for this tag are
"no”. "yes" and "no”.
:param is_closed_captioned: If the episode has closed captioning support. :param is_closed_captioned: If the episode has closed captioning
support.
:returns: If the episode has closed captioning support. :returns: If the episode has closed captioning support.
''' '''
if not itunes_is_closed_captioned is None: if itunes_is_closed_captioned is not None:
self.__itunes_is_closed_captioned = itunes_is_closed_captioned in ('yes', True) self.__itunes_is_closed_captioned = \
itunes_is_closed_captioned in ('yes', True)
return self.__itunes_is_closed_captioned return self.__itunes_is_closed_captioned
def itunes_order(self, itunes_order=None): def itunes_order(self, itunes_order=None):
'''Get or set the itunes:order value of the podcast episode. This tag can '''Get or set the itunes:order value of the podcast episode. This tag
be used to override the default ordering of episodes on the store. can be used to override the default ordering of episodes on the store.
This tag is used at an <item> level by populating with the number value This tag is used at an <item> level by populating with the number value
in which you would like the episode to appear on the store. For example, in which you would like the episode to appear on the store. For
if you would like an <item> to appear as the first episode in the example, if you would like an <item> to appear as the first episode in
podcast, you would populate the <itunes:order> tag with 1. If the podcast, you would populate the <itunes:order> tag with 1. If
conflicting order values are present in multiple episodes, the store will conflicting order values are present in multiple episodes, the store
use default ordering (pubDate). will use default ordering (pubDate).
To remove the order from the episode set the order to a value below zero. To remove the order from the episode set the order to a value below
zero.
:param itunes_order: The order of the episode. :param itunes_order: The order of the episode.
:returns: The order of the episode. :returns: The order of the episode.
''' '''
if not itunes_order is None: if itunes_order is not None:
self.__itunes_order = int(itunes_order) self.__itunes_order = int(itunes_order)
return self.__itunes_order return self.__itunes_order
def itunes_subtitle(self, itunes_subtitle=None): def itunes_subtitle(self, itunes_subtitle=None):
'''Get or set the itunes:subtitle value for the podcast episode. The '''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 contents of this tag are shown in the Description column in iTunes. The
@ -222,22 +224,21 @@ class PodcastEntryExtension(BaseEntryExtension):
:param itunes_subtitle: Subtitle of the podcast episode. :param itunes_subtitle: Subtitle of the podcast episode.
:returns: Subtitle of the podcast episode. :returns: Subtitle of the podcast episode.
''' '''
if not itunes_subtitle is None: if itunes_subtitle is not None:
self.__itunes_subtitle = itunes_subtitle self.__itunes_subtitle = itunes_subtitle
return self.__itunes_subtitle return self.__itunes_subtitle
def itunes_summary(self, itunes_summary=None): def itunes_summary(self, itunes_summary=None):
'''Get or set the itunes:summary value for the podcast episode. The '''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 contents of this tag are shown in a separate window that appears when
"circled i" in the Description column is clicked. It also appears on the the "circled i" in the Description column is clicked. It also appears
iTunes page for your podcast. This field can be up to 4000 characters. If on the iTunes page for your podcast. This field can be up to 4000
<itunes:summary> is not included, the contents of the <description> tag characters. If <itunes:summary> is not included, the contents of the
are used. <description> tag are used.
:param itunes_summary: Summary of the podcast episode. :param itunes_summary: Summary of the podcast episode.
:returns: Summary of the podcast episode. :returns: Summary of the podcast episode.
''' '''
if not itunes_summary is None: if itunes_summary is not None:
self.__itunes_summary = itunes_summary self.__itunes_summary = itunes_summary
return self.__itunes_summary return self.__itunes_summary

View file

@ -11,15 +11,16 @@
''' '''
from lxml import etree from lxml import etree
from feedgen.ext.base import BaseExtension,BaseEntryExtension from feedgen.ext.base import BaseExtension, BaseEntryExtension
TORRENT_NS = 'http://xmlns.ezrss.it/0.1/dtd/' TORRENT_NS = 'http://xmlns.ezrss.it/0.1/dtd/'
class TorrentExtension(BaseExtension): class TorrentExtension(BaseExtension):
'''FeedGenerator extension for torrent feeds. '''FeedGenerator extension for torrent feeds.
''' '''
def extend_ns(self): def extend_ns(self):
return {'torrent' : TORRENT_NS} return {'torrent': TORRENT_NS}
class TorrentEntryExtension(BaseEntryExtension): class TorrentEntryExtension(BaseEntryExtension):
@ -31,8 +32,6 @@ class TorrentEntryExtension(BaseEntryExtension):
self.__torrent_contentlength = None self.__torrent_contentlength = None
self.__torrent_seeds = None self.__torrent_seeds = None
self.__torrent_peers = None self.__torrent_peers = None
self.__torrent=verified = None
def extend_rss(self, entry): def extend_rss(self, entry):
'''Add additional fields to an RSS item. '''Add additional fields to an RSS item.
@ -44,7 +43,8 @@ class TorrentEntryExtension(BaseEntryExtension):
filename.text = self.__torrent_filename filename.text = self.__torrent_filename
if self.__torrent_contentlength: if self.__torrent_contentlength:
contentlength = etree.SubElement(entry, '{%s}contentlength' % TORRENT_NS) contentlength = etree.SubElement(entry,
'{%s}contentlength' % TORRENT_NS)
contentlength.text = self.__torrent_contentlength contentlength.text = self.__torrent_contentlength
if self.__torrent_infohash: if self.__torrent_infohash:
@ -65,63 +65,62 @@ class TorrentEntryExtension(BaseEntryExtension):
verified = etree.SubElement(entry, '{%s}verified' % TORRENT_NS) verified = etree.SubElement(entry, '{%s}verified' % TORRENT_NS)
verified.text = self.__torrent_verified verified.text = self.__torrent_verified
def filename(self, torrent_filename=None): def filename(self, torrent_filename=None):
'''Get or set the name of the torrent file. '''Get or set the name of the torrent file.
:param torrent_filename: The name of the torrent file. :param torrent_filename: The name of the torrent file.
:returns: The name of the torrent file. :returns: The name of the torrent file.
''' '''
if not torrent_filename is None: if torrent_filename is not None:
self.__torrent_filename = torrent_filename self.__torrent_filename = torrent_filename
return self.__torrent_filename return self.__torrent_filename
def infohash (self, torrent_infohash=None): def infohash(self, torrent_infohash=None):
'''Get or set the hash of the target file. '''Get or set the hash of the target file.
:param torrent_infohash: The target file hash. :param torrent_infohash: The target file hash.
:returns: The target hash file. :returns: The target hash file.
''' '''
if not torrent_infohash is None: if torrent_infohash is not None:
self.__torrent_infohash = torrent_infohash self.__torrent_infohash = torrent_infohash
return self.__torrent_infohash return self.__torrent_infohash
def contentlength (self, torrent_contentlength=None): def contentlength(self, torrent_contentlength=None):
'''Get or set the size of the target file. '''Get or set the size of the target file.
:param torrent_contentlength: The target file size. :param torrent_contentlength: The target file size.
:returns: The target file size. :returns: The target file size.
''' '''
if not torrent_contentlength is None: if torrent_contentlength is not None:
self.__torrent_contentlength = torrent_contentlength self.__torrent_contentlength = torrent_contentlength
return self.__torrent_contentlength return self.__torrent_contentlength
def seeds (self, torrent_seeds=None): def seeds(self, torrent_seeds=None):
'''Get or set the number of seeds. '''Get or set the number of seeds.
:param torrent_seeds: The seeds number. :param torrent_seeds: The seeds number.
:returns: The seeds number. :returns: The seeds number.
''' '''
if not torrent_seeds is None: if torrent_seeds is not None:
self.__torrent_seeds = torrent_seeds self.__torrent_seeds = torrent_seeds
return self.__torrent_seeds return self.__torrent_seeds
def peers (self, torrent_peers=None): def peers(self, torrent_peers=None):
'''Get or set the number od peers '''Get or set the number od peers
:param torrent_infohash: The peers number. :param torrent_infohash: The peers number.
:returns: The peers number. :returns: The peers number.
''' '''
if not torrent_peers is None: if torrent_peers is not None:
self.__torrent_peers = torrent_peers self.__torrent_peers = torrent_peers
return self.__torrent_peers return self.__torrent_peers
def verified (self, torrent_verified=None): def verified(self, torrent_verified=None):
'''Get or set the number of verified peers. '''Get or set the number of verified peers.
:param torrent_infohash: The verified peers number. :param torrent_infohash: The verified peers number.
:returns: The verified peers number. :returns: The verified peers number.
''' '''
if not torrent_verified is None: if torrent_verified is not None:
self.__torrent_verified = torrent_verified self.__torrent_verified = torrent_verified
return self.__torrent_verified return self.__torrent_verified

View file

@ -3,7 +3,7 @@
feedgen.feed feedgen.feed
~~~~~~~~~~~~ ~~~~~~~~~~~~
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013-2016, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
@ -27,11 +27,10 @@ class FeedGenerator(object):
'''FeedGenerator for generating ATOM and RSS feeds. '''FeedGenerator for generating ATOM and RSS feeds.
''' '''
def __init__(self): def __init__(self):
self.__feed_entries = [] self.__feed_entries = []
## ATOM # ATOM
# http://www.atomenabled.org/developers/syndication/ # http://www.atomenabled.org/developers/syndication/
# required # required
self.__atom_id = None self.__atom_id = None
@ -46,9 +45,9 @@ class FeedGenerator(object):
self.__atom_category = None # {term*, scheme, label} self.__atom_category = None # {term*, scheme, label}
self.__atom_contributor = None self.__atom_contributor = None
self.__atom_generator = { self.__atom_generator = {
'value' :'python-feedgen', 'value': 'python-feedgen',
'uri' :'http://lkiesow.github.io/python-feedgen', 'uri': 'http://lkiesow.github.io/python-feedgen',
'version':feedgen.version.version_str } #{value*,uri,version} 'version': feedgen.version.version_str} # {value*,uri,version}
self.__atom_icon = None self.__atom_icon = None
self.__atom_logo = None self.__atom_logo = None
self.__atom_rights = None self.__atom_rights = None
@ -57,7 +56,7 @@ class FeedGenerator(object):
# other # other
self.__atom_feed_xml_lang = None self.__atom_feed_xml_lang = None
## RSS # RSS
# http://www.rssboard.org/rss-specification # http://www.rssboard.org/rss-specification
self.__rss_title = None self.__rss_title = None
self.__rss_link = None self.__rss_link = None
@ -83,9 +82,9 @@ class FeedGenerator(object):
# Extension list: # Extension list:
self.__extensions = {} self.__extensions = {}
def _create_atom(self, extensions=True): def _create_atom(self, extensions=True):
'''Create a ATOM feed xml structure containing all previously set fields. '''Create a ATOM feed xml structure containing all previously set
fields.
:returns: Tuple containing the feed root element and the element tree. :returns: Tuple containing the feed root element and the element tree.
''' '''
@ -93,17 +92,20 @@ class FeedGenerator(object):
if extensions: if extensions:
for ext in self.__extensions.values() or []: for ext in self.__extensions.values() or []:
if ext.get('atom'): if ext.get('atom'):
nsmap.update( ext['inst'].extend_ns() ) nsmap.update(ext['inst'].extend_ns())
feed = etree.Element('feed', xmlns='http://www.w3.org/2005/Atom', nsmap=nsmap) feed = etree.Element('feed',
xmlns='http://www.w3.org/2005/Atom',
nsmap=nsmap)
if self.__atom_feed_xml_lang: if self.__atom_feed_xml_lang:
feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = \ feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = \
self.__atom_feed_xml_lang self.__atom_feed_xml_lang
if not ( self.__atom_id and self.__atom_title and self.__atom_updated ): if not (self.__atom_id and self.__atom_title and self.__atom_updated):
missing = ', '.join(([] if self.__atom_title else ['title']) + \ missing = ([] if self.__atom_title else ['title']) + \
([] if self.__atom_id else ['id']) + \ ([] if self.__atom_id else ['id']) + \
([] if self.__atom_updated else ['updated'])) ([] if self.__atom_updated else ['updated'])
missing = ', '.join(missing)
raise ValueError('Required fields not set (%s)' % missing) raise ValueError('Required fields not set (%s)' % missing)
id = etree.SubElement(feed, 'id') id = etree.SubElement(feed, 'id')
id.text = self.__atom_id id.text = self.__atom_id
@ -198,7 +200,6 @@ class FeedGenerator(object):
doc = etree.ElementTree(feed) doc = etree.ElementTree(feed)
return feed, doc return feed, doc
def atom_str(self, pretty=False, extensions=True, encoding='UTF-8', def atom_str(self, pretty=False, extensions=True, encoding='UTF-8',
xml_declaration=True): xml_declaration=True):
'''Generates an ATOM feed and returns the feed XML as string. '''Generates an ATOM feed and returns the feed XML as string.
@ -213,20 +214,19 @@ class FeedGenerator(object):
:returns: String representation of the ATOM feed. :returns: String representation of the ATOM feed.
**Return type:** The return type may vary between different Python **Return type:** The return type may vary between different Python
versions and your encoding parameters passed to this method. For details versions and your encoding parameters passed to this method. For
have a look at the `lxml documentation details have a look at the `lxml documentation
<https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring>`_ <https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring>`_
''' '''
feed, doc = self._create_atom(extensions=extensions) feed, doc = self._create_atom(extensions=extensions)
return etree.tostring(feed, pretty_print=pretty, encoding=encoding, return etree.tostring(feed, pretty_print=pretty, encoding=encoding,
xml_declaration=xml_declaration) xml_declaration=xml_declaration)
def atom_file(self, filename, extensions=True, pretty=False, def atom_file(self, filename, extensions=True, pretty=False,
encoding='UTF-8', xml_declaration=True): encoding='UTF-8', xml_declaration=True):
'''Generates an ATOM feed and write the resulting XML to a file. '''Generates an ATOM feed and write the resulting XML to a file.
:param filename: Name of file to write, or a file-like object, or a URL. :param filename: Name of file to write or a file-like object or a URL.
:param extensions: Enable or disable the loaded extensions for the xml :param extensions: Enable or disable the loaded extensions for the xml
generation (default: enabled). generation (default: enabled).
:param pretty: If the feed should be split into multiple lines and :param pretty: If the feed should be split into multiple lines and
@ -239,9 +239,9 @@ class FeedGenerator(object):
doc.write(filename, pretty_print=pretty, encoding=encoding, doc.write(filename, pretty_print=pretty, encoding=encoding,
xml_declaration=xml_declaration) xml_declaration=xml_declaration)
def _create_rss(self, extensions=True): def _create_rss(self, extensions=True):
'''Create an RSS feed xml structure containing all previously set fields. '''Create an RSS feed xml structure containing all previously set
fields.
:returns: Tuple containing the feed root element and the element tree. :returns: Tuple containing the feed root element and the element tree.
''' '''
@ -249,17 +249,20 @@ class FeedGenerator(object):
if extensions: if extensions:
for ext in self.__extensions.values() or []: for ext in self.__extensions.values() or []:
if ext.get('rss'): if ext.get('rss'):
nsmap.update( ext['inst'].extend_ns() ) nsmap.update(ext['inst'].extend_ns())
nsmap.update({'atom': 'http://www.w3.org/2005/Atom', nsmap.update({'atom': 'http://www.w3.org/2005/Atom',
'content': 'http://purl.org/rss/1.0/modules/content/'}) 'content': 'http://purl.org/rss/1.0/modules/content/'})
feed = etree.Element('rss', version='2.0', nsmap=nsmap ) feed = etree.Element('rss', version='2.0', nsmap=nsmap)
channel = etree.SubElement(feed, 'channel') channel = etree.SubElement(feed, 'channel')
if not ( self.__rss_title and self.__rss_link and self.__rss_description ): if not (self.__rss_title and
missing = ', '.join(([] if self.__rss_title else ['title']) + \ self.__rss_link and
self.__rss_description):
missing = ([] if self.__rss_title else ['title']) + \
([] if self.__rss_link else ['link']) + \ ([] if self.__rss_link else ['link']) + \
([] if self.__rss_description else ['description'])) ([] if self.__rss_description else ['description'])
missing = ', '.join(missing)
raise ValueError('Required fields not set (%s)' % missing) raise ValueError('Required fields not set (%s)' % missing)
title = etree.SubElement(channel, 'title') title = etree.SubElement(channel, 'title')
title.text = self.__rss_title title.text = self.__rss_title
@ -270,8 +273,8 @@ class FeedGenerator(object):
for ln in self.__atom_link or []: for ln in self.__atom_link or []:
# It is recommended to include a atom self link in rss documents… # It is recommended to include a atom self link in rss documents…
if ln.get('rel') == 'self': if ln.get('rel') == 'self':
selflink = etree.SubElement(channel, selflink = etree.SubElement(
'{http://www.w3.org/2005/Atom}link', channel, '{http://www.w3.org/2005/Atom}link',
href=ln['href'], rel='self') href=ln['href'], rel='self')
if ln.get('type'): if ln.get('type'):
selflink.attrib['type'] = ln['type'] selflink.attrib['type'] = ln['type']
@ -310,11 +313,9 @@ class FeedGenerator(object):
url = etree.SubElement(image, 'url') url = etree.SubElement(image, 'url')
url.text = self.__rss_image.get('url') url.text = self.__rss_image.get('url')
title = etree.SubElement(image, 'title') title = etree.SubElement(image, 'title')
title.text = self.__rss_image['title'] \ title.text = self.__rss_image.get('title', self.__rss_title)
if self.__rss_image.get('title') else self.__rss_title
link = etree.SubElement(image, 'link') link = etree.SubElement(image, 'link')
link.text = self.__rss_image['link'] \ link.text = self.__rss_image.get('link', self.__rss_link)
if self.__rss_image.get('link') else self.__rss_link
if self.__rss_image.get('width'): if self.__rss_image.get('width'):
width = etree.SubElement(image, 'width') width = etree.SubElement(image, 'width')
width.text = self.__rss_image.get('width') width.text = self.__rss_image.get('width')
@ -353,7 +354,8 @@ class FeedGenerator(object):
if self.__rss_textInput: if self.__rss_textInput:
textInput = etree.SubElement(channel, 'textInput') textInput = etree.SubElement(channel, 'textInput')
textInput.attrib['title'] = self.__rss_textInput.get('title') textInput.attrib['title'] = self.__rss_textInput.get('title')
textInput.attrib['description'] = self.__rss_textInput.get('description') textInput.attrib['description'] = \
self.__rss_textInput.get('description')
textInput.attrib['name'] = self.__rss_textInput.get('name') textInput.attrib['name'] = self.__rss_textInput.get('name')
textInput.attrib['link'] = self.__rss_textInput.get('link') textInput.attrib['link'] = self.__rss_textInput.get('link')
if self.__rss_ttl: if self.__rss_ttl:
@ -375,7 +377,6 @@ class FeedGenerator(object):
doc = etree.ElementTree(feed) doc = etree.ElementTree(feed)
return feed, doc return feed, doc
def rss_str(self, pretty=False, extensions=True, encoding='UTF-8', def rss_str(self, pretty=False, extensions=True, encoding='UTF-8',
xml_declaration=True): xml_declaration=True):
'''Generates an RSS feed and returns the feed XML as string. '''Generates an RSS feed and returns the feed XML as string.
@ -390,20 +391,19 @@ class FeedGenerator(object):
:returns: String representation of the RSS feed. :returns: String representation of the RSS feed.
**Return type:** The return type may vary between different Python **Return type:** The return type may vary between different Python
versions and your encoding parameters passed to this method. For details versions and your encoding parameters passed to this method. For
have a look at the `lxml documentation details have a look at the `lxml documentation
<https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring>`_ <https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring>`_
''' '''
feed, doc = self._create_rss(extensions=extensions) feed, doc = self._create_rss(extensions=extensions)
return etree.tostring(feed, pretty_print=pretty, encoding=encoding, return etree.tostring(feed, pretty_print=pretty, encoding=encoding,
xml_declaration=xml_declaration) xml_declaration=xml_declaration)
def rss_file(self, filename, extensions=True, pretty=False, def rss_file(self, filename, extensions=True, pretty=False,
encoding='UTF-8', xml_declaration=True): encoding='UTF-8', xml_declaration=True):
'''Generates an RSS feed and write the resulting XML to a file. '''Generates an RSS feed and write the resulting XML to a file.
:param filename: Name of file to write, or a file-like object, or a URL. :param filename: Name of file to write or a file-like object or a URL.
:param extensions: Enable or disable the loaded extensions for the xml :param extensions: Enable or disable the loaded extensions for the xml
generation (default: enabled). generation (default: enabled).
:param pretty: If the feed should be split into multiple lines and :param pretty: If the feed should be split into multiple lines and
@ -416,7 +416,6 @@ class FeedGenerator(object):
doc.write(filename, pretty_print=pretty, encoding=encoding, doc.write(filename, pretty_print=pretty, encoding=encoding,
xml_declaration=xml_declaration) xml_declaration=xml_declaration)
def title(self, title=None): def title(self, title=None):
'''Get or set the title value of the feed. It should contain a human '''Get or set the title value of the feed. It should contain a human
readable title for the feed. Often the same as the title of the readable title for the feed. Often the same as the title of the
@ -426,12 +425,11 @@ class FeedGenerator(object):
:param title: The new title of the feed. :param title: The new title of the feed.
:returns: The feeds title. :returns: The feeds title.
''' '''
if not title is None: if title is not None:
self.__atom_title = title self.__atom_title = title
self.__rss_title = title self.__rss_title = title
return self.__atom_title return self.__atom_title
def id(self, id=None): def id(self, id=None):
'''Get or set the feed id which identifies the feed using a universally '''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 unique and permanent URI. If you have a long-term, renewable lease on
@ -442,17 +440,16 @@ class FeedGenerator(object):
:returns: Id of the feed. :returns: Id of the feed.
''' '''
if not id is None: if id is not None:
self.__atom_id = id self.__atom_id = id
return self.__atom_id return self.__atom_id
def updated(self, updated=None): def updated(self, updated=None):
'''Set or get the updated value which indicates the last time the feed '''Set or get the updated value which indicates the last time the feed
was modified in a significant way. was modified in a significant way.
The value can either be a string which will automatically be parsed or a The value can either be a string which will automatically be parsed or
datetime.datetime object. In any case it is necessary that the value a datetime.datetime object. In any case it is necessary that the value
include timezone information. include timezone information.
This will set both atom:updated and rss:lastBuildDate. This will set both atom:updated and rss:lastBuildDate.
@ -463,7 +460,7 @@ class FeedGenerator(object):
:param updated: The modification date. :param updated: The modification date.
:returns: Modification date as datetime.datetime :returns: Modification date as datetime.datetime
''' '''
if not updated is None: if updated is not None:
if isinstance(updated, string_types): if isinstance(updated, string_types):
updated = dateutil.parser.parse(updated) updated = dateutil.parser.parse(updated)
if not isinstance(updated, datetime): if not isinstance(updated, datetime):
@ -475,13 +472,12 @@ class FeedGenerator(object):
return self.__atom_updated return self.__atom_updated
def lastBuildDate(self, lastBuildDate=None): def lastBuildDate(self, lastBuildDate=None):
'''Set or get the lastBuildDate value which indicates the last time the '''Set or get the lastBuildDate value which indicates the last time the
content of the channel changed. content of the channel changed.
The value can either be a string which will automatically be parsed or a The value can either be a string which will automatically be parsed or
datetime.datetime object. In any case it is necessary that the value a datetime.datetime object. In any case it is necessary that the value
include timezone information. include timezone information.
This will set both atom:updated and rss:lastBuildDate. This will set both atom:updated and rss:lastBuildDate.
@ -492,13 +488,12 @@ class FeedGenerator(object):
:param lastBuildDate: The modification date. :param lastBuildDate: The modification date.
:returns: Modification date as datetime.datetime :returns: Modification date as datetime.datetime
''' '''
return self.updated( lastBuildDate ) return self.updated(lastBuildDate)
def author(self, author=None, replace=False, **kwargs): def author(self, author=None, replace=False, **kwargs):
'''Get or set author data. An author element is a dictionary containing a name, '''Get or set author data. An author element is a dictionary containing
an email address and a URI. Name is mandatory for ATOM, email is mandatory a name, an email address and a URI. Name is mandatory for ATOM, email
for RSS. is mandatory for RSS.
This method can be called with: This method can be called with:
@ -518,34 +513,36 @@ class FeedGenerator(object):
Example:: Example::
>>> feedgen.author( { 'name':'John Doe', 'email':'jdoe@example.com' } ) >>> feedgen.author({'name':'John Doe', 'email':'jdoe@example.com'})
[{'name':'John Doe','email':'jdoe@example.com'}] [{'name':'John Doe','email':'jdoe@example.com'}]
>>> feedgen.author([{'name':'Mr. X'},{'name':'Max'}]) >>> feedgen.author([{'name':'Mr. X'},{'name':'Max'}])
[{'name':'John Doe','email':'jdoe@example.com'}, [{'name':'John Doe','email':'jdoe@example.com'},
{'name':'John Doe'}, {'name':'Max'}] {'name':'John Doe'}, {'name':'Max'}]
>>> feedgen.author( name='John Doe', email='jdoe@example.com', replace=True ) >>> feedgen.author(name='John Doe', email='jdoe@example.com',
replace=True)
[{'name':'John Doe','email':'jdoe@example.com'}] [{'name':'John Doe','email':'jdoe@example.com'}]
''' '''
if author is None and kwargs: if author is None and kwargs:
author = kwargs author = kwargs
if not author is None: if author is not None:
if replace or self.__atom_author is None: if replace or self.__atom_author is None:
self.__atom_author = [] self.__atom_author = []
self.__atom_author += ensure_format( author, self.__atom_author += ensure_format(author,
set(['name', 'email', 'uri']), set(['name'])) set(['name', 'email', 'uri']),
set(['name']))
self.__rss_author = [] self.__rss_author = []
for a in self.__atom_author: for a in self.__atom_author:
if a.get('email'): if a.get('email'):
self.__rss_author.append(a['email']) self.__rss_author.append(a['email'])
return self.__atom_author return self.__atom_author
def link(self, link=None, replace=False, **kwargs): def link(self, link=None, replace=False, **kwargs):
'''Get or set link data. An link element is a dict with the fields href, '''Get or set link data. An link element is a dict with the fields
rel, type, hreflang, title, and length. Href is mandatory for ATOM. href, rel, type, hreflang, title, and length. Href is mandatory for
ATOM.
This method can be called with: This method can be called with:
@ -560,8 +557,8 @@ class FeedGenerator(object):
or one of the following predefined values (default=alternate): or one of the following predefined values (default=alternate):
- *alternate* an alternate representation of the entry or feed, for - *alternate* an alternate representation of the entry or feed, for
example a permalink to the html version of the entry, or the front example a permalink to the html version of the entry, or the
page of the weblog. front page of the weblog.
- *enclosure* a related resource which is potentially large in size - *enclosure* a related resource which is potentially large in size
and might require special handling, for example an audio or video and might require special handling, for example an audio or video
recording. recording.
@ -589,24 +586,30 @@ class FeedGenerator(object):
''' '''
if link is None and kwargs: if link is None and kwargs:
link = kwargs link = kwargs
if not link is None: if link is not None:
if replace or self.__atom_link is None: if replace or self.__atom_link is None:
self.__atom_link = [] self.__atom_link = []
self.__atom_link += ensure_format( link, self.__atom_link += ensure_format(
link,
set(['href', 'rel', 'type', 'hreflang', 'title', 'length']), set(['href', 'rel', 'type', 'hreflang', 'title', 'length']),
set(['href']), set(['href']),
{'rel': [ {'rel': [
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark', 'about', 'alternate', 'appendix', 'archives', 'author',
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form', 'bookmark', 'canonical', 'chapter', 'collection',
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure', 'contents', 'copyright', 'create-form', 'current',
'duplicate', 'edit', 'edit-form', 'edit-media', 'enclosure', 'first', 'glossary', 'derivedfrom', 'describedby', 'describes', 'disclosure',
'help', 'hosts', 'hub', 'icon', 'index', 'item', 'last', 'latest-version', 'license', 'duplicate', 'edit', 'edit-form', 'edit-media',
'lrdd', 'memento', 'monitor', 'monitor-group', 'next', 'next-archive', 'nofollow', 'enclosure', 'first', 'glossary', 'help', 'hosts', 'hub',
'noreferrer', 'original', 'payment', 'predecessor-version', 'prefetch', 'prev', 'preview', 'icon', 'index', 'item', 'last', 'latest-version',
'previous', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies', 'search', 'license', 'lrdd', 'memento', 'monitor', 'monitor-group',
'section', 'self', 'service', 'start', 'stylesheet', 'subsection', 'successor-version', 'next', 'next-archive', 'nofollow', 'noreferrer',
'tag', 'terms-of-service', 'timegate', 'timemap', 'type', 'up', 'version-history', 'via', 'original', 'payment', 'predecessor-version', 'prefetch',
'working-copy', 'working-copy-of' '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: # RSS only needs one URL. We use the first link for RSS:
if len(self.__atom_link) > 0: if len(self.__atom_link) > 0:
@ -614,7 +617,6 @@ class FeedGenerator(object):
# return the set with more information (atom) # return the set with more information (atom)
return self.__atom_link return self.__atom_link
def category(self, category=None, replace=False, **kwargs): def category(self, category=None, replace=False, **kwargs):
'''Get or set categories that the feed belongs to. '''Get or set categories that the feed belongs to.
@ -630,8 +632,8 @@ class FeedGenerator(object):
- *scheme* identifies the categorization scheme via a URI. - *scheme* identifies the categorization scheme via a URI.
- *label* provides a human-readable label for display - *label* provides a human-readable label for display
If a label is present it is used for the RSS feeds. Otherwise the term is If a label is present it is used for the RSS feeds. Otherwise the term
used. The scheme is used for the domain attribute in RSS. is used. The scheme is used for the domain attribute in RSS.
:param link: Dict or list of dicts with data. :param link: Dict or list of dicts with data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
@ -639,31 +641,30 @@ class FeedGenerator(object):
''' '''
if category is None and kwargs: if category is None and kwargs:
category = kwargs category = kwargs
if not category is None: if category is not None:
if replace or self.__atom_category is None: if replace or self.__atom_category is None:
self.__atom_category = [] self.__atom_category = []
self.__atom_category += ensure_format( self.__atom_category += ensure_format(
category, category,
set(['term', 'scheme', 'label']), set(['term', 'scheme', 'label']),
set(['term']) ) set(['term']))
# Map the ATOM categories to RSS categories. Use the atom:label as # Map the ATOM categories to RSS categories. Use the atom:label as
# name or if not present the atom:term. The atom:scheme is the # name or if not present the atom:term. The atom:scheme is the
# rss:domain. # rss:domain.
self.__rss_category = [] self.__rss_category = []
for cat in self.__atom_category: for cat in self.__atom_category:
rss_cat = {} rss_cat = {}
rss_cat['value'] = cat['label'] if cat.get('label') else cat['term'] rss_cat['value'] = cat.get('label', cat['term'])
if cat.get('scheme'): if cat.get('scheme'):
rss_cat['domain'] = cat['scheme'] rss_cat['domain'] = cat['scheme']
self.__rss_category.append( rss_cat ) self.__rss_category.append(rss_cat)
return self.__atom_category return self.__atom_category
def cloud(self, domain=None, port=None, path=None, registerProcedure=None, def cloud(self, domain=None, port=None, path=None, registerProcedure=None,
protocol=None): protocol=None):
'''Set or get the cloud data of the feed. It is an RSS only attribute. It '''Set or get the cloud data of the feed. It is an RSS only attribute.
specifies a web service that supports the rssCloud interface which can be It specifies a web service that supports the rssCloud interface which
implemented in HTTP-POST, XML-RPC or SOAP 1.1. can be implemented in HTTP-POST, XML-RPC or SOAP 1.1.
:param domain: The domain where the webservice can be found. :param domain: The domain where the webservice can be found.
:param port: The port the webservice listens to. :param port: The port the webservice listens to.
@ -672,12 +673,12 @@ class FeedGenerator(object):
:param protocol: Can be either HTTP-POST, XML-RPC or SOAP 1.1. :param protocol: Can be either HTTP-POST, XML-RPC or SOAP 1.1.
:returns: Dictionary containing the cloud data. :returns: Dictionary containing the cloud data.
''' '''
if not domain is None: if domain is not None:
self.__rss_cloud = {'domain':domain, 'port':port, 'path':path, self.__rss_cloud = {'domain': domain, 'port': port, 'path': path,
'registerProcedure':registerProcedure, 'protocol':protocol} 'registerProcedure': registerProcedure,
'protocol': protocol}
return self.__rss_cloud return self.__rss_cloud
def contributor(self, contributor=None, replace=False, **kwargs): def contributor(self, contributor=None, replace=False, **kwargs):
'''Get or set the contributor data of the feed. This is an ATOM only '''Get or set the contributor data of the feed. This is an ATOM only
value. value.
@ -692,74 +693,73 @@ class FeedGenerator(object):
- *uri* contains a home page for the person. - *uri* contains a home page for the person.
- *email* contains an email address for the person. - *email* contains an email address for the person.
:param contributor: Dictionary or list of dictionaries with contributor data. :param contributor: Dictionary or list of dictionaries with contributor
data.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of contributors as dictionaries. :returns: List of contributors as dictionaries.
''' '''
if contributor is None and kwargs: if contributor is None and kwargs:
contributor = kwargs contributor = kwargs
if not contributor is None: if contributor is not None:
if replace or self.__atom_contributor is None: if replace or self.__atom_contributor is None:
self.__atom_contributor = [] self.__atom_contributor = []
self.__atom_contributor += ensure_format( contributor, self.__atom_contributor += ensure_format(
set(['name', 'email', 'uri']), set(['name'])) contributor, set(['name', 'email', 'uri']), set(['name']))
return self.__atom_contributor return self.__atom_contributor
def generator(self, generator=None, version=None, uri=None): def generator(self, generator=None, version=None, uri=None):
'''Get or the generator of the feed which identifies the software used to '''Get or the generator of the feed which identifies the software used
generate the feed, for debugging and other purposes. Both the uri and to generate the feed, for debugging and other purposes. Both the uri
version attributes are optional and only available in the ATOM feed. and version attributes are optional and only available in the ATOM
feed.
:param generator: Software used to create the feed. :param generator: Software used to create the feed.
:param version: Version of the software. :param version: Version of the software.
:param uri: URI the software can be found. :param uri: URI the software can be found.
''' '''
if not generator is None: if generator is not None:
self.__atom_generator = {'value':generator} self.__atom_generator = {'value': generator}
if not version is None: if version is not None:
self.__atom_generator['version'] = version self.__atom_generator['version'] = version
if not uri is None: if uri is not None:
self.__atom_generator['uri'] = uri self.__atom_generator['uri'] = uri
self.__rss_generator = generator self.__rss_generator = generator
return self.__atom_generator return self.__atom_generator
def icon(self, icon=None): def icon(self, icon=None):
'''Get or set the icon of the feed which is a small image which provides '''Get or set the icon of the feed which is a small image which
iconic visual identification for the feed. Icons should be square. This provides iconic visual identification for the feed. Icons should be
is an ATOM only value. square. This is an ATOM only value.
:param icon: URI of the feeds icon. :param icon: URI of the feeds icon.
:returns: URI of the feeds icon. :returns: URI of the feeds icon.
''' '''
if not icon is None: if icon is not None:
self.__atom_icon = icon self.__atom_icon = icon
return self.__atom_icon return self.__atom_icon
def logo(self, logo=None): def logo(self, logo=None):
'''Get or set the logo of the feed which is a larger image which provides '''Get or set the logo of the feed which is a larger image which
visual identification for the feed. Images should be twice as wide as provides visual identification for the feed. Images should be twice as
they are tall. This is an ATOM value but will also set the rss:image wide as they are tall. This is an ATOM value but will also set the
value. rss:image value.
:param logo: Logo of the feed. :param logo: Logo of the feed.
:returns: Logo of the feed. :returns: Logo of the feed.
''' '''
if not logo is None: if logo is not None:
self.__atom_logo = logo self.__atom_logo = logo
self.__rss_image = { 'url' : logo } self.__rss_image = {'url': logo}
return self.__atom_logo return self.__atom_logo
def image(self, url=None, title=None, link=None, width=None, height=None, def image(self, url=None, title=None, link=None, width=None, height=None,
description=None): description=None):
'''Set the image of the feed. This element is roughly equivalent to '''Set the image of the feed. This element is roughly equivalent to
atom:logo. atom:logo.
:param url: The URL of a GIF, JPEG or PNG image. :param url: The URL of a GIF, JPEG or PNG image.
:param title: Describes the image. The default value is the feeds title. :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 :param link: URL of the site the image will link to. The default is to
use the feeds first altertate link. use the feeds first altertate link.
:param width: Width of the image in pixel. The maximum is 144. :param width: Width of the image in pixel. The maximum is 144.
@ -767,11 +767,11 @@ class FeedGenerator(object):
:param description: Title of the link. :param description: Title of the link.
:returns: Data of the image as dictionary. :returns: Data of the image as dictionary.
''' '''
if not url is None: if url is not None:
self.__rss_image = { 'url' : url } self.__rss_image = {'url': url}
if not title is None: if title is not None:
self.__rss_image['title'] = title self.__rss_image['title'] = title
if not link is None: if link is not None:
self.__rss_image['link'] = link self.__rss_image['link'] = link
if width: if width:
self.__rss_image['width'] = width self.__rss_image['width'] = width
@ -780,20 +780,18 @@ class FeedGenerator(object):
self.__atom_logo = url self.__atom_logo = url
return self.__rss_image return self.__rss_image
def rights(self, rights=None): def rights(self, rights=None):
'''Get or set the rights value of the feed which conveys information '''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 about rights, e.g. copyrights, held in and over the feed. This ATOM
will also set rss:copyright. value will also set rss:copyright.
:param rights: Rights information of the feed. :param rights: Rights information of the feed.
''' '''
if not rights is None: if rights is not None:
self.__atom_rights = rights self.__atom_rights = rights
self.__rss_copyright = rights self.__rss_copyright = rights
return self.__atom_rights return self.__atom_rights
def copyright(self, copyright=None): def copyright(self, copyright=None):
'''Get or set the copyright notice for content in the channel. This RSS '''Get or set the copyright notice for content in the channel. This RSS
value will also set the atom:rights value. value will also set the atom:rights value.
@ -801,8 +799,7 @@ class FeedGenerator(object):
:param copyright: The copyright notice. :param copyright: The copyright notice.
:returns: The copyright notice. :returns: The copyright notice.
''' '''
return self.rights( copyright ) return self.rights(copyright)
def subtitle(self, subtitle=None): def subtitle(self, subtitle=None):
'''Get or set the subtitle value of the cannel which contains a '''Get or set the subtitle value of the cannel which contains a
@ -812,79 +809,75 @@ class FeedGenerator(object):
:param subtitle: The subtitle of the feed. :param subtitle: The subtitle of the feed.
:returns: The subtitle of the feed. :returns: The subtitle of the feed.
''' '''
if not subtitle is None: if subtitle is not None:
self.__atom_subtitle = subtitle self.__atom_subtitle = subtitle
self.__rss_description = subtitle self.__rss_description = subtitle
return self.__atom_subtitle return self.__atom_subtitle
def description(self, description=None): def description(self, description=None):
'''Set and get the description of the feed. This is an RSS only element '''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 which is a phrase or sentence describing the channel. It is mandatory
RSS feeds. It is roughly the same as atom:subtitle. Thus setting this for RSS feeds. It is roughly the same as atom:subtitle. Thus setting
will also set atom:subtitle. this will also set atom:subtitle.
:param description: Description of the channel. :param description: Description of the channel.
:returns: Description of the channel. :returns: Description of the channel.
''' '''
return self.subtitle( description ) return self.subtitle(description)
def docs(self, docs=None): def docs(self, docs=None):
'''Get or set the docs value of the feed. This is an RSS only value. It '''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 is a URL that points to the documentation for the format used in the
file. It is probably a pointer to [1]. It is for people who might stumble RSS file. It is probably a pointer to [1]. It is for people who might
across an RSS file on a Web server 25 years from now and wonder what it stumble across an RSS file on a Web server 25 years from now and wonder
is. what it is.
[1]: http://www.rssboard.org/rss-specification [1]: http://www.rssboard.org/rss-specification
:param docs: URL of the format documentation. :param docs: URL of the format documentation.
:returns: URL of the format documentation. :returns: URL of the format documentation.
''' '''
if not docs is None: if docs is not None:
self.__rss_docs = docs self.__rss_docs = docs
return self.__rss_docs return self.__rss_docs
def language(self, language=None): def language(self, language=None):
'''Get or set the language of the feed. It indicates the language the '''Get or set the language of the feed. It indicates the language the
channel is written in. This allows aggregators to group all Italian 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. language sites, for example, on a single page. This is an RSS only
However, this value will also be used to set the xml:lang property of the field. However, this value will also be used to set the xml:lang
ATOM feed node. property of the ATOM feed node.
The value should be an IETF language tag. The value should be an IETF language tag.
:param language: Language of the feed. :param language: Language of the feed.
:returns: Language of the feed. :returns: Language of the feed.
''' '''
if not language is None: if language is not None:
self.__rss_language = language self.__rss_language = language
self.__atom_feed_xml_lang = language self.__atom_feed_xml_lang = language
return self.__rss_language return self.__rss_language
def managingEditor(self, managingEditor=None): def managingEditor(self, managingEditor=None):
'''Set or get the value for managingEditor which is the email address for '''Set or get the value for managingEditor which is the email address
person responsible for editorial content. This is a RSS only value. for person responsible for editorial content. This is a RSS only
value.
:param managingEditor: Email adress of the managing editor. :param managingEditor: Email adress of the managing editor.
:returns: Email adress of the managing editor. :returns: Email adress of the managing editor.
''' '''
if not managingEditor is None: if managingEditor is not None:
self.__rss_managingEditor = managingEditor self.__rss_managingEditor = managingEditor
return self.__rss_managingEditor return self.__rss_managingEditor
def pubDate(self, pubDate=None): def pubDate(self, pubDate=None):
'''Set or get the publication date for the content in the channel. For '''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 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 date flips once every 24 hours. That's when the pubDate of the channel
changes. changes.
The value can either be a string which will automatically be parsed or a The value can either be a string which will automatically be parsed or
datetime.datetime object. In any case it is necessary that the value a datetime.datetime object. In any case it is necessary that the value
include timezone information. include timezone information.
This will set both atom:updated and rss:lastBuildDate. This will set both atom:updated and rss:lastBuildDate.
@ -892,7 +885,7 @@ class FeedGenerator(object):
:param pubDate: The publication date. :param pubDate: The publication date.
:returns: Publication date as datetime.datetime :returns: Publication date as datetime.datetime
''' '''
if not pubDate is None: if pubDate is not None:
if isinstance(pubDate, string_types): if isinstance(pubDate, string_types):
pubDate = dateutil.parser.parse(pubDate) pubDate = dateutil.parser.parse(pubDate)
if not isinstance(pubDate, datetime): if not isinstance(pubDate, datetime):
@ -903,76 +896,73 @@ class FeedGenerator(object):
return self.__rss_pubDate return self.__rss_pubDate
def rating(self, rating=None): def rating(self, rating=None):
'''Set and get the PICS rating for the channel. It is an RSS only '''Set and get the PICS rating for the channel. It is an RSS only
value. value.
''' '''
if not rating is None: if rating is not None:
self.__rss_rating = rating self.__rss_rating = rating
return self.__rss_rating return self.__rss_rating
def skipHours(self, hours=None, replace=False): def skipHours(self, hours=None, replace=False):
'''Set or get the value of skipHours, a hint for aggregators telling them '''Set or get the value of skipHours, a hint for aggregators telling
which hours they can skip. This is an RSS only value. 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 This method can be called with an hour or a list of hours. The hours
represented as integer values from 0 to 23. are represented as integer values from 0 to 23.
:param hours: List of hours the feedreaders should not check the feed. :param hours: List of hours the feedreaders should not check the feed.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of hours the feedreaders should not check the feed. :returns: List of hours the feedreaders should not check the feed.
''' '''
if not hours is None: if hours is not None:
if not (isinstance(hours, list) or isinstance(hours, set)): if not (isinstance(hours, list) or isinstance(hours, set)):
hours = [hours] hours = [hours]
for h in hours: for h in hours:
if not h in range(24): if h not in range(24):
raise ValueError('Invalid hour %s' % h) raise ValueError('Invalid hour %s' % h)
if replace or not self.__rss_skipHours: if replace or not self.__rss_skipHours:
self.__rss_skipHours = set() self.__rss_skipHours = set()
self.__rss_skipHours |= set(hours) self.__rss_skipHours |= set(hours)
return self.__rss_skipHours return self.__rss_skipHours
def skipDays(self, days=None, replace=False): def skipDays(self, days=None, replace=False):
'''Set or get the value of skipDays, a hint for aggregators telling them '''Set or get the value of skipDays, a hint for aggregators telling
which days they can skip This is an RSS only value. 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 This method can be called with a day name or a list of day names. The
represented as strings from 'Monday' to 'Sunday'. days are represented as strings from 'Monday' to 'Sunday'.
:param hours: List of days the feedreaders should not check the feed. :param hours: List of days the feedreaders should not check the feed.
:param replace: Add or replace old data. :param replace: Add or replace old data.
:returns: List of days the feedreaders should not check the feed. :returns: List of days the feedreaders should not check the feed.
''' '''
if not days is None: if days is not None:
if not (isinstance(days, list) or isinstance(days, set)): if not (isinstance(days, list) or isinstance(days, set)):
days = [days] days = [days]
for d in days: for d in days:
if not d in ['Monday', 'Tuesday', 'Wednesday', 'Thursday', if d not in ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday', 'Sunday']: 'Friday', 'Saturday', 'Sunday']:
raise ValueError('Invalid day %s' % h) raise ValueError('Invalid day %s' % d)
if replace or not self.__rss_skipDays: if replace or not self.__rss_skipDays:
self.__rss_skipDays = set() self.__rss_skipDays = set()
self.__rss_skipDays |= set(days) self.__rss_skipDays |= set(days)
return self.__rss_skipDays return self.__rss_skipDays
def textInput(self, title=None, description=None, name=None, link=None): 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 '''Get or set the value of textInput. This is an RSS only field. The
purpose of the <textInput> element is something of a mystery. You can use purpose of the <textInput> element is something of a mystery. You can
it to specify a search engine box. Or to allow a reader to provide use it to specify a search engine box. Or to allow a reader to provide
feedback. Most aggregators ignore it. feedback. Most aggregators ignore it.
:param title: The label of the Submit button in the text input area. :param title: The label of the Submit button in the text input area.
:param description: Explains 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 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. :param link: The URL of the CGI script that processes text input
requests.
:returns: Dictionary containing textInput values. :returns: Dictionary containing textInput values.
''' '''
if not title is None: if title is not None:
self.__rss_textInput = {} self.__rss_textInput = {}
self.__rss_textInput['title'] = title self.__rss_textInput['title'] = title
self.__rss_textInput['description'] = description self.__rss_textInput['description'] = description
@ -980,37 +970,35 @@ class FeedGenerator(object):
self.__rss_textInput['link'] = link self.__rss_textInput['link'] = link
return self.__rss_textInput return self.__rss_textInput
def ttl(self, ttl=None): def ttl(self, ttl=None):
'''Get or set the ttl value. It is an RSS only element. ttl stands for '''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 time to live. It's a number of minutes that indicates how long a
can be cached before refreshing from the source. channel can be cached before refreshing from the source.
:param ttl: Integer value indicating how long the channel may be cached. :param ttl: Integer value indicating how long the channel may be
cached.
:returns: Time to live. :returns: Time to live.
''' '''
if not ttl is None: if ttl is not None:
self.__rss_ttl = int(ttl) self.__rss_ttl = int(ttl)
return self.__rss_ttl return self.__rss_ttl
def webMaster(self, webMaster=None): def webMaster(self, webMaster=None):
'''Get and set the value of webMaster, which represents the email address '''Get and set the value of webMaster, which represents the email
for the person responsible for technical issues relating to the feed. address for the person responsible for technical issues relating to the
This is an RSS only value. feed. This is an RSS only value.
:param webMaster: Email address of the webmaster. :param webMaster: Email address of the webmaster.
:returns: Email address of the webmaster. :returns: Email address of the webmaster.
''' '''
if not webMaster is None: if webMaster is not None:
self.__rss_webMaster = webMaster self.__rss_webMaster = webMaster
return self.__rss_webMaster return self.__rss_webMaster
def add_entry(self, feedEntry=None): def add_entry(self, feedEntry=None):
'''This method will add a new entry to the feed. If the feedEntry '''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 argument is omittet a new Entry object is created automatically. This
the prefered way to add new entries to a feed. is the prefered way to add new entries to a feed.
:param feedEntry: FeedEntry object to add. :param feedEntry: FeedEntry object to add.
:returns: FeedEntry object created or passed to this function. :returns: FeedEntry object created or passed to this function.
@ -1033,18 +1021,18 @@ class FeedGenerator(object):
items = self.__extensions.items() items = self.__extensions.items()
# Try to load extensions: # Try to load extensions:
for extname,ext in items: for extname, ext in items:
try: try:
feedEntry.register_extension(extname, feedEntry.register_extension(extname,
ext['extension_class_entry'], ext['extension_class_entry'],
ext['atom'], ext['rss'] ) ext['atom'],
ext['rss'])
except ImportError: except ImportError:
pass pass
self.__feed_entries.append( feedEntry ) self.__feed_entries.append(feedEntry)
return feedEntry return feedEntry
def add_item(self, item=None): def add_item(self, item=None):
'''This method will add a new item to the feed. If the item argument is '''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 omittet a new FeedEntry object is created automatically. This is just
@ -1052,7 +1040,6 @@ class FeedGenerator(object):
''' '''
return self.add_entry(item) return self.add_entry(item)
def entry(self, entry=None, replace=False): def entry(self, entry=None, replace=False):
'''Get or set feed entries. Use the add_entry() method instead to '''Get or set feed entries. Use the add_entry() method instead to
automatically create the FeedEntry objects. automatically create the FeedEntry objects.
@ -1062,7 +1049,7 @@ class FeedGenerator(object):
:param entry: FeedEntry object or list of FeedEntry objects. :param entry: FeedEntry object or list of FeedEntry objects.
:returns: List ob all feed entries. :returns: List ob all feed entries.
''' '''
if not entry is None: if entry is not None:
if not isinstance(entry, list): if not isinstance(entry, list):
entry = [entry] entry = [entry]
if replace: if replace:
@ -1077,24 +1064,22 @@ class FeedGenerator(object):
# Try to load extensions: # Try to load extensions:
for e in entry: for e in entry:
for extname,ext in items: for extname, ext in items:
try: try:
e.register_extension(extname, e.register_extension(extname,
ext['extension_class_entry'], ext['extension_class_entry'],
ext['atom'], ext['rss'] ) ext['atom'], ext['rss'])
except ImportError: except ImportError:
pass pass
self.__feed_entries += entry self.__feed_entries += entry
return self.__feed_entries return self.__feed_entries
def item(self, item=None, replace=False): def item(self, item=None, replace=False):
'''Get or set feed items. This is just another name for entry(...) '''Get or set feed items. This is just another name for entry(...)
''' '''
return self.entry(item, replace) return self.entry(item, replace)
def remove_entry(self, entry): def remove_entry(self, entry):
'''Remove a single entry from the feed. This method accepts both the '''Remove a single entry from the feed. This method accepts both the
FeedEntry object to remove or the index of the entry as argument. FeedEntry object to remove or the index of the entry as argument.
@ -1106,14 +1091,12 @@ class FeedGenerator(object):
else: else:
self.__feed_entries.pop(entry) self.__feed_entries.pop(entry)
def remove_item(self, item): def remove_item(self, item):
'''Remove a single item from the feed. This is another name for '''Remove a single item from the feed. This is another name for
remove_entry. remove_entry.
''' '''
self.remove_entry(item) self.remove_entry(item)
def load_extension(self, name, atom=True, rss=True): def load_extension(self, name, atom=True, rss=True):
'''Load a specific extension by name. '''Load a specific extension by name.
@ -1145,9 +1128,8 @@ class FeedGenerator(object):
entryext = None entryext = None
self.register_extension(name, feedext, entryext, atom, rss) self.register_extension(name, feedext, entryext, atom, rss)
def register_extension(self, namespace, extension_class_feed=None,
def register_extension(self, namespace, extension_class_feed = None, extension_class_entry=None, atom=True, rss=True):
extension_class_entry = None, atom=True, rss=True):
'''Registers an extension by class. '''Registers an extension by class.
:param namespace: namespace for the extension :param namespace: namespace for the extension
@ -1169,17 +1151,19 @@ class FeedGenerator(object):
# `load_extension` registry # `load_extension` registry
self.__extensions[namespace] = { self.__extensions[namespace] = {
'inst':extinst, 'inst': extinst,
'extension_class_feed': extension_class_feed, 'extension_class_feed': extension_class_feed,
'extension_class_entry': extension_class_entry, 'extension_class_entry': extension_class_entry,
'atom':atom, 'atom': atom,
'rss':rss 'rss': rss
} }
# Try to load the extension for already existing entries: # Try to load the extension for already existing entries:
for entry in self.__feed_entries: for entry in self.__feed_entries:
try: try:
entry.register_extension(namespace, entry.register_extension(namespace,
extension_class_entry, atom, rss) extension_class_entry,
atom,
rss)
except ImportError: except ImportError:
pass pass

View file

@ -8,7 +8,8 @@
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
import sys, locale import locale
import sys
def ensure_format(val, allowed, required, allowed_values=None, defaults=None): def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
@ -19,7 +20,8 @@ def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
:param val: Dictionaries to check. :param val: Dictionaries to check.
:param allowed: Set of allowed keys. :param allowed: Set of allowed keys.
:param required: Set of required keys. :param required: Set of required keys.
:param allowed_values: Dictionary with keys and sets of their allowed values. :param allowed_values: Dictionary with keys and sets of their allowed
values.
:param defaults: Dictionary with default values. :param defaults: Dictionary with default values.
:returns: List of checked dictionaries. :returns: List of checked dictionaries.
''' '''
@ -44,7 +46,7 @@ def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
else: else:
items = defaults.items() items = defaults.items()
for k,v in items: for k, v in items:
elem[k] = elem.get(k, v) elem[k] = elem.get(k, v)
if not set(elem.keys()) <= allowed: if not set(elem.keys()) <= allowed:
raise ValueError('Data contains invalid keys') raise ValueError('Data contains invalid keys')
@ -56,9 +58,9 @@ def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
else: else:
values = allowed_values.items() values = allowed_values.items()
for k,v in values: for k, v in values:
if elem.get(k) and not elem[k] in v: if elem.get(k) and not elem[k] in v:
raise ValueError('Invalid value for %s' % k ) raise ValueError('Invalid value for %s' % k)
return val return val

View file

@ -6,24 +6,24 @@ import feedgen.version
packages = ['feedgen', 'feedgen/ext'] packages = ['feedgen', 'feedgen/ext']
setup( setup(name='feedgen',
name = 'feedgen', packages=packages,
packages = packages, version=feedgen.version.version_full_str,
version = feedgen.version.version_full_str, description='Feed Generator (ATOM, RSS, Podcasts)',
description = 'Feed Generator (ATOM, RSS, Podcasts)', author='Lars Kiesow',
author = 'Lars Kiesow', author_email='lkiesow@uos.de',
author_email = 'lkiesow@uos.de', url='http://lkiesow.github.io/python-feedgen',
url = 'http://lkiesow.github.io/python-feedgen', keywords=['feed', 'ATOM', 'RSS', 'podcast'],
keywords = ['feed','ATOM','RSS','podcast'], license='FreeBSD and LGPLv3+',
license = 'FreeBSD and LGPLv3+', install_requires=['lxml', 'python-dateutil'],
install_requires = ['lxml', 'python-dateutil'], classifiers=[
classifiers = [
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: Information Technology', 'Intended Audience :: Information Technology',
'Intended Audience :: Science/Research', 'Intended Audience :: Science/Research',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', 'License :: OSI Approved :: GNU Lesser General Public License v3 ' +
'or later (LGPLv3+)',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
@ -35,7 +35,7 @@ setup(
'Topic :: Text Processing :: Markup', 'Topic :: Text Processing :: Markup',
'Topic :: Text Processing :: Markup :: XML' 'Topic :: Text Processing :: Markup :: XML'
], ],
long_description = '''\ long_description='''\
Feedgenerator Feedgenerator
============= =============
@ -46,5 +46,4 @@ Podcasts.
It is licensed under the terms of both, the FreeBSD license and the LGPLv3+. It is licensed under the terms of both, the FreeBSD license and the LGPLv3+.
Choose the one which is more convenient for you. For more details have a look Choose the one which is more convenient for you. For more details have a look
at license.bsd and license.lgpl. at license.bsd and license.lgpl.
''' ''')
)

View file

@ -7,9 +7,9 @@ These are test cases for a basic entry.
""" """
import unittest import unittest
from lxml import etree
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
class TestSequenceFunctions(unittest.TestCase): class TestSequenceFunctions(unittest.TestCase):
def setUp(self): def setUp(self):
@ -25,7 +25,7 @@ class TestSequenceFunctions(unittest.TestCase):
fe.id('http://lernfunk.de/media/654321/1') fe.id('http://lernfunk.de/media/654321/1')
fe.title('The First Episode') fe.title('The First Episode')
#Use also the different name add_item # Use also the different name add_item
fe = fg.add_item() fe = fg.add_item()
fe.id('http://lernfunk.de/media/654321/1') fe.id('http://lernfunk.de/media/654321/1')
fe.title('The Second Episode') fe.title('The Second Episode')
@ -49,7 +49,7 @@ class TestSequenceFunctions(unittest.TestCase):
def test_checkEntryContent(self): def test_checkEntryContent(self):
fg = self.fg fg = self.fg
assert len(fg.entry()) != None assert fg.entry()
def test_removeEntryByIndex(self): def test_removeEntryByIndex(self):
fg = FeedGenerator() fg = FeedGenerator()
@ -79,15 +79,15 @@ class TestSequenceFunctions(unittest.TestCase):
def test_categoryHasDomain(self): def test_categoryHasDomain(self):
fg = FeedGenerator() fg = FeedGenerator()
fg.title('some title') fg.title('some title')
fg.link( href='http://www.dontcare.com', rel='alternate' ) fg.link(href='http://www.dontcare.com', rel='alternate')
fg.description('description') fg.description('description')
fe = fg.add_entry() fe = fg.add_entry()
fe.id('http://lernfunk.de/media/654321/1') fe.id('http://lernfunk.de/media/654321/1')
fe.title('some title') fe.title('some title')
fe.category([ fe.category([
{'term' : 'category', {'term': 'category',
'scheme': 'http://www.somedomain.com/category', 'scheme': 'http://www.somedomain.com/category',
'label' : 'Category', 'label': 'Category',
}]) }])
result = fg.rss_str() result = fg.rss_str()
@ -103,4 +103,3 @@ class TestSequenceFunctions(unittest.TestCase):
fe.content('content', type='CDATA') fe.content('content', type='CDATA')
result = fg.atom_str() result = fg.atom_str()
assert b'<content type="CDATA"><![CDATA[content]]></content>' in result assert b'<content type="CDATA"><![CDATA[content]]></content>' in result

View file

@ -12,6 +12,8 @@ from lxml import etree
class TestExtensionSyndication(unittest.TestCase): class TestExtensionSyndication(unittest.TestCase):
SYN_NS = {'sy': 'http://purl.org/rss/1.0/modules/syndication/'}
def setUp(self): def setUp(self):
self.fg = FeedGenerator() self.fg = FeedGenerator()
self.fg.load_extension('syndication') self.fg.load_extension('syndication')
@ -20,14 +22,11 @@ class TestExtensionSyndication(unittest.TestCase):
self.fg.description('description') self.fg.description('description')
def test_update_period(self): def test_update_period(self):
for period_type in ('hourly', 'daily', 'weekly', for period_type in ('hourly', 'daily', 'weekly', 'monthly', 'yearly'):
'monthly', 'yearly'):
self.fg.syndication.update_period(period_type) self.fg.syndication.update_period(period_type)
root = etree.fromstring(self.fg.rss_str()) root = etree.fromstring(self.fg.rss_str())
a = root.xpath('/rss/channel/sy:UpdatePeriod', a = root.xpath('/rss/channel/sy:UpdatePeriod',
namespaces={ namespaces=self.SYN_NS)
'sy':'http://purl.org/rss/1.0/modules/syndication/'
})
assert a[0].text == period_type assert a[0].text == period_type
def test_update_frequency(self): def test_update_frequency(self):
@ -35,19 +34,14 @@ class TestExtensionSyndication(unittest.TestCase):
self.fg.syndication.update_frequency(frequency) self.fg.syndication.update_frequency(frequency)
root = etree.fromstring(self.fg.rss_str()) root = etree.fromstring(self.fg.rss_str())
a = root.xpath('/rss/channel/sy:UpdateFrequency', a = root.xpath('/rss/channel/sy:UpdateFrequency',
namespaces={ namespaces=self.SYN_NS)
'sy':'http://purl.org/rss/1.0/modules/syndication/'
})
assert a[0].text == str(frequency) assert a[0].text == str(frequency)
def test_update_base(self): def test_update_base(self):
base = '2000-01-01T12:00+00:00' base = '2000-01-01T12:00+00:00'
self.fg.syndication.update_base(base) self.fg.syndication.update_base(base)
root = etree.fromstring(self.fg.rss_str()) root = etree.fromstring(self.fg.rss_str())
a = root.xpath('/rss/channel/sy:UpdateBase', a = root.xpath('/rss/channel/sy:UpdateBase', namespaces=self.SYN_NS)
namespaces={
'sy':'http://purl.org/rss/1.0/modules/syndication/'
})
assert a[0].text == base assert a[0].text == base
@ -61,13 +55,13 @@ class TestExtensionPodcast(unittest.TestCase):
self.fg.description('description') self.fg.description('description')
def test_category_new(self): def test_category_new(self):
self.fg.podcast.itunes_category([{'cat':'Technology', self.fg.podcast.itunes_category([{'cat': 'Technology',
'sub':'Podcasting'}]) 'sub': 'Podcasting'}])
self.fg.podcast.itunes_explicit('no') self.fg.podcast.itunes_explicit('no')
self.fg.podcast.itunes_complete('no') self.fg.podcast.itunes_complete('no')
self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss') self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss')
self.fg.podcast.itunes_owner('John Doe', 'john@example.com') self.fg.podcast.itunes_owner('John Doe', 'john@example.com')
ns = {'itunes':'http://www.itunes.com/dtds/podcast-1.0.dtd'} ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
root = etree.fromstring(self.fg.rss_str()) root = etree.fromstring(self.fg.rss_str())
cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns) cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns)
scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text', scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text',
@ -81,7 +75,7 @@ class TestExtensionPodcast(unittest.TestCase):
self.fg.podcast.itunes_complete('no') self.fg.podcast.itunes_complete('no')
self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss') self.fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss')
self.fg.podcast.itunes_owner('John Doe', 'john@example.com') self.fg.podcast.itunes_owner('John Doe', 'john@example.com')
ns = {'itunes':'http://www.itunes.com/dtds/podcast-1.0.dtd'} ns = {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
root = etree.fromstring(self.fg.rss_str()) root = etree.fromstring(self.fg.rss_str())
cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns) cat = root.xpath('/rss/channel/itunes:category/@text', namespaces=ns)
scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text', scat = root.xpath('/rss/channel/itunes:category/itunes:category/@text',

View file

@ -12,6 +12,7 @@ from lxml import etree
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from feedgen.ext.dc import DcExtension, DcEntryExtension from feedgen.ext.dc import DcExtension, DcEntryExtension
class TestSequenceFunctions(unittest.TestCase): class TestSequenceFunctions(unittest.TestCase):
def setUp(self): def setUp(self):
@ -26,7 +27,7 @@ class TestSequenceFunctions(unittest.TestCase):
self.authorName = 'John Doe' self.authorName = 'John Doe'
self.authorMail = 'john@example.de' self.authorMail = 'john@example.de'
self.author = {'name': self.authorName,'email': self.authorMail} self.author = {'name': self.authorName, 'email': self.authorMail}
self.linkHref = 'http://example.com' self.linkHref = 'http://example.com'
self.linkRel = 'alternate' self.linkRel = 'alternate'
@ -50,12 +51,14 @@ class TestSequenceFunctions(unittest.TestCase):
self.cloudProtocol = 'SOAP 1.1' self.cloudProtocol = 'SOAP 1.1'
self.icon = "http://example.com/icon.png" self.icon = "http://example.com/icon.png"
self.contributor = {'name':"Contributor Name", 'uri':"Contributor Uri", self.contributor = {'name': "Contributor Name",
'uri': "Contributor Uri",
'email': 'Contributor email'} 'email': 'Contributor email'}
self.copyright = "The copyright notice" self.copyright = "The copyright notice"
self.docs = 'http://www.rssboard.org/rss-specification' self.docs = 'http://www.rssboard.org/rss-specification'
self.managingEditor = 'mail@example.com' self.managingEditor = 'mail@example.com'
self.rating = '(PICS-1.1 "http://www.classify.org/safesurf/" 1 r (SS~~000 1))' self.rating = '(PICS-1.1 "http://www.classify.org/safesurf/" ' + \
'1 r (SS~~000 1))'
self.skipDays = 'Tuesday' self.skipDays = 'Tuesday'
self.skipHours = 23 self.skipHours = 23
@ -71,13 +74,14 @@ class TestSequenceFunctions(unittest.TestCase):
fg.id(self.feedId) fg.id(self.feedId)
fg.title(self.title) fg.title(self.title)
fg.author(self.author) fg.author(self.author)
fg.link( href=self.linkHref, rel=self.linkRel ) fg.link(href=self.linkHref, rel=self.linkRel)
fg.logo(self.logo) fg.logo(self.logo)
fg.subtitle(self.subtitle) fg.subtitle(self.subtitle)
fg.link( href=self.link2Href, rel=self.link2Rel ) fg.link(href=self.link2Href, rel=self.link2Rel)
fg.language(self.language) fg.language(self.language)
fg.cloud(domain=self.cloudDomain, port=self.cloudPort, fg.cloud(domain=self.cloudDomain, port=self.cloudPort,
path=self.cloudPath, registerProcedure=self.cloudRegisterProcedure, path=self.cloudPath,
registerProcedure=self.cloudRegisterProcedure,
protocol=self.cloudProtocol) protocol=self.cloudProtocol)
fg.icon(self.icon) fg.icon(self.icon)
fg.category(term=self.categoryTerm, scheme=self.categoryScheme, fg.category(term=self.categoryTerm, scheme=self.categoryScheme,
@ -90,14 +94,13 @@ class TestSequenceFunctions(unittest.TestCase):
fg.skipDays(self.skipDays) fg.skipDays(self.skipDays)
fg.skipHours(self.skipHours) fg.skipHours(self.skipHours)
fg.textInput(title=self.textInputTitle, fg.textInput(title=self.textInputTitle,
description=self.textInputDescription, name=self.textInputName, description=self.textInputDescription,
link=self.textInputLink) name=self.textInputName, link=self.textInputLink)
fg.ttl(self.ttl) fg.ttl(self.ttl)
fg.webMaster(self.webMaster) fg.webMaster(self.webMaster)
self.fg = fg self.fg = fg
def test_baseFeed(self): def test_baseFeed(self):
fg = self.fg fg = self.fg
@ -123,8 +126,8 @@ class TestSequenceFunctions(unittest.TestCase):
filename = 'tmp_Atomfeed.xml' filename = 'tmp_Atomfeed.xml'
fg.atom_file(filename=filename, pretty=True, xml_declaration=False) fg.atom_file(filename=filename, pretty=True, xml_declaration=False)
with open (filename, "r") as myfile: with open(filename, "r") as myfile:
atomString=myfile.read().replace('\n', '') atomString = myfile.read().replace('\n', '')
self.checkAtomString(atomString) self.checkAtomString(atomString)
@ -137,18 +140,22 @@ class TestSequenceFunctions(unittest.TestCase):
def test_rel_values_for_atom(self): def test_rel_values_for_atom(self):
values_for_rel = [ values_for_rel = [
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark', 'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark',
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form', 'canonical', 'chapter', 'collection', 'contents', 'copyright',
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure', 'create-form', 'current', 'derivedfrom', 'describedby',
'duplicate', 'edit', 'edit-form', 'edit-media', 'enclosure', 'first', 'glossary', 'describes', 'disclosure', 'duplicate', 'edit', 'edit-form',
'help', 'hosts', 'hub', 'icon', 'index', 'item', 'last', 'latest-version', 'license', 'edit-media', 'enclosure', 'first', 'glossary', 'help', 'hosts',
'lrdd', 'memento', 'monitor', 'monitor-group', 'next', 'next-archive', 'nofollow', 'hub', 'icon', 'index', 'item', 'last', 'latest-version',
'noreferrer', 'original', 'payment', 'predecessor-version', 'prefetch', 'prev', 'preview', 'license', 'lrdd', 'memento', 'monitor', 'monitor-group', 'next',
'previous', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies', 'search', 'next-archive', 'nofollow', 'noreferrer', 'original', 'payment',
'section', 'self', 'service', 'start', 'stylesheet', 'subsection', 'successor-version', 'predecessor-version', 'prefetch', 'prev', 'preview', 'previous',
'tag', 'terms-of-service', 'timegate', 'timemap', 'type', 'up', 'version-history', 'via', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies',
'working-copy', 'working-copy-of' 'search', 'section', 'self', 'service', 'start', 'stylesheet',
] 'subsection', 'successor-version', 'tag', 'terms-of-service',
links = [{'href': '%s/%s' % (self.linkHref, val.replace('-', '_')), 'rel': val} for val in values_for_rel] 'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
'working-copy', 'working-copy-of']
links = [{'href': '%s/%s' % (self.linkHref,
val.replace('-', '_')), 'rel': val}
for val in values_for_rel]
fg = self.fg fg = self.fg
fg.link(links, replace=True) fg.link(links, replace=True)
atomString = fg.atom_str(pretty=True, xml_declaration=False) atomString = fg.atom_str(pretty=True, xml_declaration=False)
@ -165,18 +172,22 @@ class TestSequenceFunctions(unittest.TestCase):
def test_rel_values_for_rss(self): def test_rel_values_for_rss(self):
values_for_rel = [ values_for_rel = [
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark', 'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark',
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form', 'canonical', 'chapter', 'collection', 'contents', 'copyright',
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure', 'create-form', 'current', 'derivedfrom', 'describedby',
'duplicate', 'edit', 'edit-form', 'edit-media', 'enclosure', 'first', 'glossary', 'describes', 'disclosure', 'duplicate', 'edit', 'edit-form',
'help', 'hosts', 'hub', 'icon', 'index', 'item', 'last', 'latest-version', 'license', 'edit-media', 'enclosure', 'first', 'glossary', 'help', 'hosts',
'lrdd', 'memento', 'monitor', 'monitor-group', 'next', 'next-archive', 'nofollow', 'hub', 'icon', 'index', 'item', 'last', 'latest-version',
'noreferrer', 'original', 'payment', 'predecessor-version', 'prefetch', 'prev', 'preview', 'license', 'lrdd', 'memento', 'monitor', 'monitor-group', 'next',
'previous', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies', 'search', 'next-archive', 'nofollow', 'noreferrer', 'original', 'payment',
'section', 'self', 'service', 'start', 'stylesheet', 'subsection', 'successor-version', 'predecessor-version', 'prefetch', 'prev', 'preview', 'previous',
'tag', 'terms-of-service', 'timegate', 'timemap', 'type', 'up', 'version-history', 'via', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies',
'working-copy', 'working-copy-of' 'search', 'section', 'self', 'service', 'start', 'stylesheet',
] 'subsection', 'successor-version', 'tag', 'terms-of-service',
links = [{'href': '%s/%s' % (self.linkHref, val.replace('-', '_')), 'rel': val} for val in values_for_rel] 'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
'working-copy', 'working-copy-of']
links = [{'href': '%s/%s' % (self.linkHref,
val.replace('-', '_')), 'rel': val}
for val in values_for_rel]
fg = self.fg fg = self.fg
fg.link(links, replace=True) fg.link(links, replace=True)
rssString = fg.rss_str(pretty=True, xml_declaration=False) rssString = fg.rss_str(pretty=True, xml_declaration=False)
@ -185,13 +196,16 @@ class TestSequenceFunctions(unittest.TestCase):
nsAtom = self.nsAtom nsAtom = self.nsAtom
atom_links = channel.findall("{%s}link" % nsAtom) atom_links = channel.findall("{%s}link" % nsAtom)
assert len(atom_links) == 1 # rss feed only implements atom's 'self' link # rss feed only implements atom's 'self' link
assert len(atom_links) == 1
assert atom_links[0].get('href') == '%s/%s' % (self.linkHref, 'self') assert atom_links[0].get('href') == '%s/%s' % (self.linkHref, 'self')
assert atom_links[0].get('rel') == 'self' assert atom_links[0].get('rel') == 'self'
rss_links = channel.findall('link') rss_links = channel.findall('link')
assert len(rss_links) == 1 # RSS only needs one URL. We use the first link for RSS: # RSS only needs one URL. We use the first link for RSS:
assert rss_links[0].text == '%s/%s' % (self.linkHref, 'working-copy-of'.replace('-', '_')) assert len(rss_links) == 1
assert rss_links[0].text == '%s/%s' % \
(self.linkHref, 'working-copy-of'.replace('-', '_'))
def checkAtomString(self, atomString): def checkAtomString(self, atomString):
@ -200,22 +214,31 @@ class TestSequenceFunctions(unittest.TestCase):
nsAtom = self.nsAtom nsAtom = self.nsAtom
assert feed.find("{%s}title" % nsAtom).text == self.title assert feed.find("{%s}title" % nsAtom).text == self.title
assert feed.find("{%s}updated" % nsAtom).text != None assert feed.find("{%s}updated" % nsAtom).text is not None
assert feed.find("{%s}id" % nsAtom).text == self.feedId assert feed.find("{%s}id" % nsAtom).text == self.feedId
assert feed.find("{%s}category" % nsAtom).get('term') == self.categoryTerm assert feed.find("{%s}category" % nsAtom)\
assert feed.find("{%s}category" % nsAtom).get('label') == self.categoryLabel .get('term') == self.categoryTerm
assert feed.find("{%s}author" % nsAtom).find("{%s}name" % nsAtom).text == self.authorName assert feed.find("{%s}category" % nsAtom)\
assert feed.find("{%s}author" % nsAtom).find("{%s}email" % nsAtom).text == self.authorMail .get('label') == self.categoryLabel
assert feed.findall("{%s}link" % nsAtom)[0].get('href') == self.linkHref assert feed.find("{%s}author" % nsAtom)\
.find("{%s}name" % nsAtom).text == self.authorName
assert feed.find("{%s}author" % nsAtom)\
.find("{%s}email" % nsAtom).text == self.authorMail
assert feed.findall("{%s}link" % nsAtom)[0]\
.get('href') == self.linkHref
assert feed.findall("{%s}link" % nsAtom)[0].get('rel') == self.linkRel assert feed.findall("{%s}link" % nsAtom)[0].get('rel') == self.linkRel
assert feed.findall("{%s}link" % nsAtom)[1].get('href') == self.link2Href assert feed.findall("{%s}link" % nsAtom)[1]\
.get('href') == self.link2Href
assert feed.findall("{%s}link" % nsAtom)[1].get('rel') == self.link2Rel assert feed.findall("{%s}link" % nsAtom)[1].get('rel') == self.link2Rel
assert feed.find("{%s}logo" % nsAtom).text == self.logo assert feed.find("{%s}logo" % nsAtom).text == self.logo
assert feed.find("{%s}icon" % nsAtom).text == self.icon assert feed.find("{%s}icon" % nsAtom).text == self.icon
assert feed.find("{%s}subtitle" % nsAtom).text == self.subtitle assert feed.find("{%s}subtitle" % nsAtom).text == self.subtitle
assert feed.find("{%s}contributor" % nsAtom).find("{%s}name" % nsAtom).text == self.contributor['name'] assert feed.find("{%s}contributor" % nsAtom)\
assert feed.find("{%s}contributor" % nsAtom).find("{%s}email" % nsAtom).text == self.contributor['email'] .find("{%s}name" % nsAtom).text == self.contributor['name']
assert feed.find("{%s}contributor" % nsAtom).find("{%s}uri" % nsAtom).text == self.contributor['uri'] assert feed.find("{%s}contributor" % nsAtom)\
.find("{%s}email" % nsAtom).text == self.contributor['email']
assert feed.find("{%s}contributor" % nsAtom)\
.find("{%s}uri" % nsAtom).text == self.contributor['uri']
assert feed.find("{%s}rights" % nsAtom).text == self.copyright assert feed.find("{%s}rights" % nsAtom).text == self.copyright
def test_rssFeedFile(self): def test_rssFeedFile(self):
@ -223,8 +246,8 @@ class TestSequenceFunctions(unittest.TestCase):
filename = 'tmp_Rssfeed.xml' filename = 'tmp_Rssfeed.xml'
fg.rss_file(filename=filename, pretty=True, xml_declaration=False) fg.rss_file(filename=filename, pretty=True, xml_declaration=False)
with open (filename, "r") as myfile: with open(filename, "r") as myfile:
rssString=myfile.read().replace('\n', '') rssString = myfile.read().replace('\n', '')
self.checkRssString(rssString) self.checkRssString(rssString)
@ -248,7 +271,7 @@ class TestSequenceFunctions(unittest.TestCase):
def test_extensionAlreadyLoaded(self): def test_extensionAlreadyLoaded(self):
fg = self.fg fg = self.fg
fg.load_extension('dc', atom=True, rss=True) fg.load_extension('dc', atom=True, rss=True)
with self.assertRaises(ImportError) as context: with self.assertRaises(ImportError):
fg.load_extension('dc') fg.load_extension('dc')
def test_registerCustomExtension(self): def test_registerCustomExtension(self):
@ -261,39 +284,42 @@ class TestSequenceFunctions(unittest.TestCase):
feed = etree.fromstring(rssString) feed = etree.fromstring(rssString)
nsAtom = self.nsAtom nsAtom = self.nsAtom
nsRss = self.nsRss
channel = feed.find("channel") ch = feed.find("channel")
assert channel != None assert ch is not None
assert ch.find("title").text == self.title
assert ch.find("description").text == self.subtitle
assert ch.find("lastBuildDate").text is not None
docs = "http://www.rssboard.org/rss-specification"
assert ch.find("docs").text == docs
assert ch.find("generator").text == "python-feedgen"
assert ch.findall("{%s}link" % nsAtom)[0].get('href') == self.link2Href
assert ch.findall("{%s}link" % nsAtom)[0].get('rel') == self.link2Rel
assert ch.find("image").find("url").text == self.logo
assert ch.find("image").find("title").text == self.title
assert ch.find("image").find("link").text == self.link2Href
assert ch.find("category").text == self.categoryLabel
assert ch.find("cloud").get('domain') == self.cloudDomain
assert ch.find("cloud").get('port') == self.cloudPort
assert ch.find("cloud").get('path') == self.cloudPath
assert ch.find("cloud").get('registerProcedure') == \
self.cloudRegisterProcedure
assert ch.find("cloud").get('protocol') == self.cloudProtocol
assert ch.find("copyright").text == self.copyright
assert ch.find("docs").text == self.docs
assert ch.find("managingEditor").text == self.managingEditor
assert ch.find("rating").text == self.rating
assert ch.find("skipDays").find("day").text == self.skipDays
assert int(ch.find("skipHours").find("hour").text) == self.skipHours
assert ch.find("textInput").get('title') == self.textInputTitle
assert ch.find("textInput").get('description') == \
self.textInputDescription
assert ch.find("textInput").get('name') == self.textInputName
assert ch.find("textInput").get('link') == self.textInputLink
assert int(ch.find("ttl").text) == self.ttl
assert ch.find("webMaster").text == self.webMaster
assert channel.find("title").text == self.title
assert channel.find("description").text == self.subtitle
assert channel.find("lastBuildDate").text != None
assert channel.find("docs").text == "http://www.rssboard.org/rss-specification"
assert channel.find("generator").text == "python-feedgen"
assert channel.findall("{%s}link" % nsAtom)[0].get('href') == self.link2Href
assert channel.findall("{%s}link" % nsAtom)[0].get('rel') == self.link2Rel
assert channel.find("image").find("url").text == self.logo
assert channel.find("image").find("title").text == self.title
assert channel.find("image").find("link").text == self.link2Href
assert channel.find("category").text == self.categoryLabel
assert channel.find("cloud").get('domain') == self.cloudDomain
assert channel.find("cloud").get('port') == self.cloudPort
assert channel.find("cloud").get('path') == self.cloudPath
assert channel.find("cloud").get('registerProcedure') == self.cloudRegisterProcedure
assert channel.find("cloud").get('protocol') == self.cloudProtocol
assert channel.find("copyright").text == self.copyright
assert channel.find("docs").text == self.docs
assert channel.find("managingEditor").text == self.managingEditor
assert channel.find("rating").text == self.rating
assert channel.find("skipDays").find("day").text == self.skipDays
assert int(channel.find("skipHours").find("hour").text) == self.skipHours
assert channel.find("textInput").get('title') == self.textInputTitle
assert channel.find("textInput").get('description') == self.textInputDescription
assert channel.find("textInput").get('name') == self.textInputName
assert channel.find("textInput").get('link') == self.textInputLink
assert int(channel.find("ttl").text) == self.ttl
assert channel.find("webMaster").text == self.webMaster
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()