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:
parent
ccf18502bc
commit
444855a248
19 changed files with 3553 additions and 3572 deletions
1
Makefile
1
Makefile
|
@ -53,3 +53,4 @@ test:
|
|||
python -m unittest tests.test_entry
|
||||
python -m unittest tests.test_extension
|
||||
@rm -f tmp_Atomfeed.xml tmp_Rssfeed.xml
|
||||
flake8 $$(find setup.py tests feedgen -name '*.py')
|
||||
|
|
52
doc/conf.py
52
doc/conf.py
|
@ -3,7 +3,10 @@
|
|||
# All configuration values have a default; values that are commented out
|
||||
# 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,
|
||||
# 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
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.coverage',
|
||||
|
@ -65,7 +68,8 @@ release = feedgen.version.version_full_str
|
|||
# directories to ignore when looking for source files.
|
||||
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
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
|
@ -86,7 +90,7 @@ pygments_style = 'sphinx'
|
|||
#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
|
||||
# a list of builtin themes.
|
||||
|
@ -169,7 +173,7 @@ html_static_path = ['_static']
|
|||
htmlhelp_basename = 'pyFeedGen'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
# -- Options for LaTeX output -------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
|
@ -182,11 +186,11 @@ latex_elements = {
|
|||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
# Grouping the document tree into LaTeX files. List of tuples (source start
|
||||
# file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'pyFeedGen.tex', u'pyFeedGen Documentation',
|
||||
u'Lars Kiesow', 'manual'),
|
||||
('index', 'pyFeedGen.tex', u'pyFeedGen Documentation', u'Lars Kiesow',
|
||||
'manual'),
|
||||
]
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
# -- Options for manual page output -------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
|
@ -223,7 +227,7 @@ man_pages = [
|
|||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
# -- Options for Texinfo output -----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
|
@ -248,28 +252,11 @@ texinfo_documents = [
|
|||
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
|
||||
r = re.compile(r'\[`*([^\]`]+)`*\]\(([^\)]+)\)')
|
||||
r2 = re.compile(r'.. include-github-readme')
|
||||
|
||||
|
||||
def substitute_link(app, docname, text):
|
||||
if docname == 'index':
|
||||
readme_text = ''
|
||||
|
@ -279,5 +266,4 @@ def substitute_link(app, docname, text):
|
|||
|
||||
|
||||
def setup(app):
|
||||
app.connect('autodoc-process-docstring', process_docstring)
|
||||
app.connect('source-read', substitute_link)
|
||||
|
|
|
@ -41,13 +41,14 @@
|
|||
|
||||
>>> 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
|
||||
-----------------
|
||||
|
||||
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
|
||||
>>> rssfeed = fg.rss_str(pretty=True) # Get the RSS feed as string
|
||||
|
@ -77,8 +78,8 @@
|
|||
Extensions
|
||||
----------
|
||||
|
||||
The FeedGenerator supports extension to include additional data into the XML
|
||||
structure of the feeds. Extensions can be loaded like this::
|
||||
The FeedGenerator supports extension to include additional data into the
|
||||
XML structure of the feeds. Extensions can be loaded like this::
|
||||
|
||||
>>> fg.load_extension('someext', atom=True, rss=True)
|
||||
|
||||
|
@ -94,14 +95,14 @@
|
|||
`ext/someext_entry.py` which is suggested especially for large extensions.
|
||||
|
||||
The parameters `atom` and `rss` tell the FeedGenerator if the extensions
|
||||
should only be used for either ATOM or RSS feeds. The default value for both
|
||||
parameters is true which means that the extension would be used for both
|
||||
kinds of feeds.
|
||||
should only be used for either ATOM or RSS feeds. The default value for
|
||||
both parameters is true which means that the extension would be used for
|
||||
both kinds of feeds.
|
||||
|
||||
**Example: Produceing a Podcast**
|
||||
|
||||
One extension already provided is the podcast extension. A podcast is an RSS
|
||||
feed with some additional elements for ITunes.
|
||||
One extension already provided is the podcast extension. A podcast is an
|
||||
RSS feed with some additional elements for ITunes.
|
||||
|
||||
To produce a podcast simply load the `podcast` extension::
|
||||
|
||||
|
|
|
@ -11,37 +11,46 @@
|
|||
from feedgen.feed import FeedGenerator
|
||||
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):
|
||||
'''Print function compatible with both python2 and python3 accepting strings
|
||||
and byte arrays.
|
||||
'''
|
||||
if sys.version_info[0] >= 3:
|
||||
print(s.decode('utf-8') if type(s) == type(b'') else s)
|
||||
print(s.decode('utf-8') if isinstance(s, bytes) else s)
|
||||
else:
|
||||
print(s)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2 or not (
|
||||
sys.argv[1].endswith('rss') \
|
||||
or sys.argv[1].endswith('atom') \
|
||||
or sys.argv[1].endswith('torrent') \
|
||||
or sys.argv[1].endswith('podcast') ):
|
||||
print_enc ('Usage: %s ( <file>.atom | atom | <file>.rss | rss | podcast | torrent )' % \
|
||||
'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 ('')
|
||||
sys.argv[1].endswith('rss') or
|
||||
sys.argv[1].endswith('atom') or
|
||||
sys.argv[1] == 'torrent' or
|
||||
sys.argv[1] == 'podcast'):
|
||||
print(USAGE)
|
||||
exit()
|
||||
|
||||
arg = sys.argv[1]
|
||||
|
@ -64,11 +73,11 @@ if __name__ == '__main__':
|
|||
fe.id('http://lernfunk.de/_MEDIAID_123#1')
|
||||
fe.title('First Element')
|
||||
fe.content('''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Tamen
|
||||
aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si ista
|
||||
mala sunt, placet. Aut etiam, ut vestitum, sic sententiam habeas aliam
|
||||
domesticam, aliam forensem, ut in fronte ostentatio sit, intus veritas
|
||||
occultetur? Cum id fugiunt, re eadem defendunt, quae Peripatetici,
|
||||
verba.''')
|
||||
aberramus a proposito, et, ne longius, prorsus, inquam, Piso, si
|
||||
ista mala sunt, placet. Aut etiam, ut vestitum, sic sententiam
|
||||
habeas aliam domesticam, aliam forensem, ut in fronte ostentatio
|
||||
sit, intus veritas occultetur? Cum id fugiunt, re eadem defendunt,
|
||||
quae Peripatetici, verba.''')
|
||||
fe.summary(u'Lorem ipsum dolor sit amet, consectetur adipiscing elit…')
|
||||
fe.link(href='http://example.com', rel='alternate')
|
||||
fe.author(name='Lars Kiesow', email='lkiesow@uos.de')
|
||||
|
@ -87,15 +96,17 @@ if __name__ == '__main__':
|
|||
fg.podcast.itunes_complete('no')
|
||||
fg.podcast.itunes_new_feed_url('http://example.com/new-feed.rss')
|
||||
fg.podcast.itunes_owner('John Doe', 'john@example.com')
|
||||
fg.podcast.itunes_summary('Lorem ipsum dolor sit amet, ' + \
|
||||
'consectetur adipiscing elit. ' + \
|
||||
'Verba tu fingas et ea dicas, quae non sentias?')
|
||||
fg.podcast.itunes_summary('Lorem ipsum dolor sit amet, consectetur ' +
|
||||
'adipiscing elit. Verba tu fingas et ea ' +
|
||||
'dicas, quae non sentias?')
|
||||
fe.podcast.itunes_author('Lars Kiesow')
|
||||
print_enc(fg.rss_str(pretty=True))
|
||||
|
||||
elif arg == 'torrent':
|
||||
fg.load_extension('torrent')
|
||||
fe.link( href='http://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.infohash('7661229811ef32014879ceedcdf4a48f256c88ba')
|
||||
fe.torrent.contentlength('331350016')
|
||||
|
|
|
@ -4,4 +4,4 @@ import sys
|
|||
if sys.version_info[0] >= 3:
|
||||
string_types = str
|
||||
else:
|
||||
string_types = basestring
|
||||
string_types = basestring # noqa: F821
|
||||
|
|
219
feedgen/entry.py
219
feedgen/entry.py
|
@ -58,7 +58,6 @@ class FeedEntry(object):
|
|||
self.__extensions = {}
|
||||
self.__extensions_register = {}
|
||||
|
||||
|
||||
def atom_entry(self, extensions=True):
|
||||
'''Create an ATOM entry and return it.'''
|
||||
entry = etree.Element('entry')
|
||||
|
@ -71,12 +70,13 @@ class FeedEntry(object):
|
|||
updated = etree.SubElement(entry, 'updated')
|
||||
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 True in [ l.get('rel') == 'alternate' \
|
||||
for l in self.__atom_link or [] ]:
|
||||
raise ValueError('Entry must contain an alternate link or '
|
||||
+ 'a content element.')
|
||||
links = self.__atom_link or []
|
||||
if not [l for l in links if l.get('rel') == 'alternate']:
|
||||
raise ValueError('Entry must contain an alternate link or ' +
|
||||
'a content element.')
|
||||
|
||||
# Add author elements
|
||||
for a in self.__atom_author or []:
|
||||
|
@ -101,21 +101,24 @@ class FeedEntry(object):
|
|||
elif self.__atom_content.get('content'):
|
||||
# Surround xhtml with a div tag, parse it and embed it
|
||||
if type == 'xhtml':
|
||||
content.append(etree.fromstring('''<div
|
||||
xmlns="http://www.w3.org/1999/xhtml">%s</div>''' % \
|
||||
self.__atom_content.get('content')))
|
||||
content.append(etree.fromstring(
|
||||
'<div xmlns="http://www.w3.org/1999/xhtml">' +
|
||||
self.__atom_content.get('content') + '</div>'))
|
||||
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
|
||||
elif not type or type.startswith('text') or type == 'html':
|
||||
content.text = self.__atom_content.get('content')
|
||||
# Parse XML and embed it
|
||||
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
|
||||
else:
|
||||
raise ValueError('base64 encoded content is not supported at the moment.'
|
||||
+ 'If you are interested , please file a bug report.')
|
||||
raise ValueError('base64 encoded content is not ' +
|
||||
'supported at the moment. Pull requests' +
|
||||
' adding support are welcome.')
|
||||
# Add type description of the content
|
||||
if type:
|
||||
content.attrib['type'] = type
|
||||
|
@ -149,7 +152,7 @@ class FeedEntry(object):
|
|||
# Atom requires a name. Skip elements without.
|
||||
if not c.get('name'):
|
||||
continue
|
||||
contrib = etree.SubElement(feed, 'contributor')
|
||||
contrib = etree.SubElement(entry, 'contributor')
|
||||
name = etree.SubElement(contrib, 'name')
|
||||
name.text = c.get('name')
|
||||
if c.get('email'):
|
||||
|
@ -164,7 +167,7 @@ class FeedEntry(object):
|
|||
published.text = self.__atom_published.isoformat()
|
||||
|
||||
if self.__atom_rights:
|
||||
rights = etree.SubElement(feed, 'rights')
|
||||
rights = etree.SubElement(entry, 'rights')
|
||||
rights.text = self.__atom_rights
|
||||
|
||||
if extensions:
|
||||
|
@ -174,11 +177,12 @@ class FeedEntry(object):
|
|||
|
||||
return entry
|
||||
|
||||
|
||||
def rss_entry(self, extensions=True):
|
||||
'''Create a RSS item and return it.'''
|
||||
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')
|
||||
if self.__rss_title:
|
||||
title = etree.SubElement(entry, 'title')
|
||||
|
@ -189,10 +193,11 @@ class FeedEntry(object):
|
|||
if self.__rss_description and self.__rss_content:
|
||||
description = etree.SubElement(entry, 'description')
|
||||
description.text = self.__rss_description
|
||||
content = etree.SubElement(entry, '{%s}encoded' %
|
||||
'http://purl.org/rss/1.0/modules/content/')
|
||||
XMLNS_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']) \
|
||||
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:
|
||||
description = etree.SubElement(entry, 'description')
|
||||
description.text = self.__rss_description
|
||||
|
@ -230,8 +235,6 @@ class FeedEntry(object):
|
|||
|
||||
return entry
|
||||
|
||||
|
||||
|
||||
def title(self, title=None):
|
||||
'''Get or set the title value of the entry. It should contain a human
|
||||
readable title for the entry. Title is mandatory for both ATOM and RSS
|
||||
|
@ -240,49 +243,47 @@ class FeedEntry(object):
|
|||
:param title: The new title of the entry.
|
||||
:returns: The entriess title.
|
||||
'''
|
||||
if not title is None:
|
||||
if title is not None:
|
||||
self.__atom_title = title
|
||||
self.__rss_title = title
|
||||
return self.__atom_title
|
||||
|
||||
|
||||
def id(self, id=None):
|
||||
'''Get or set the entry id which identifies the entry using a universally
|
||||
unique and permanent URI. Two entries in a feed can have the same value
|
||||
for id if they represent the same entry at different points in time. This
|
||||
method will also set rss:guid. Id is mandatory for an ATOM entry.
|
||||
'''Get or set the entry id which identifies the entry using a
|
||||
universally unique and permanent URI. Two entries in a feed can have
|
||||
the same value for id if they represent the same entry at different
|
||||
points in time. This method will also set rss:guid. Id is mandatory
|
||||
for an ATOM entry.
|
||||
|
||||
:param id: New Id of the entry.
|
||||
:returns: Id of the entry.
|
||||
'''
|
||||
if not id is None:
|
||||
if id is not None:
|
||||
self.__atom_id = id
|
||||
self.__rss_guid = id
|
||||
return self.__atom_id
|
||||
|
||||
|
||||
def guid(self, guid=None):
|
||||
'''Get or set the entries guid which is a string that uniquely identifies
|
||||
the item. This will also set atom:id.
|
||||
'''Get or set the entries guid which is a string that uniquely
|
||||
identifies the item. This will also set atom:id.
|
||||
|
||||
:param guid: Id of the entry.
|
||||
:returns: Id of the entry.
|
||||
'''
|
||||
return self.id(guid)
|
||||
|
||||
|
||||
def updated(self, updated=None):
|
||||
'''Set or get the updated value which indicates the last time the entry
|
||||
was modified in a significant way.
|
||||
|
||||
The value can either be a string which will automatically be parsed or a
|
||||
datetime.datetime object. In any case it is necessary that the value
|
||||
The value can either be a string which will automatically be parsed or
|
||||
a datetime.datetime object. In any case it is necessary that the value
|
||||
include timezone information.
|
||||
|
||||
:param updated: The modification date.
|
||||
:returns: Modification date as datetime.datetime
|
||||
'''
|
||||
if not updated is None:
|
||||
if updated is not None:
|
||||
if isinstance(updated, string_types):
|
||||
updated = dateutil.parser.parse(updated)
|
||||
if not isinstance(updated, datetime):
|
||||
|
@ -294,11 +295,10 @@ class FeedEntry(object):
|
|||
|
||||
return self.__atom_updated
|
||||
|
||||
|
||||
def author(self, author=None, replace=False, **kwargs):
|
||||
'''Get or set autor data. An author element is a dict containing a name,
|
||||
an email adress and a uri. Name is mandatory for ATOM, email is mandatory
|
||||
for RSS.
|
||||
'''Get or set autor data. An author element is a dict containing a
|
||||
name, an email adress and a uri. Name is mandatory for ATOM, email is
|
||||
mandatory for RSS.
|
||||
|
||||
This method can be called with:
|
||||
- the fields of an author as keyword arguments
|
||||
|
@ -328,23 +328,23 @@ class FeedEntry(object):
|
|||
'''
|
||||
if author is None and kwargs:
|
||||
author = kwargs
|
||||
if not author is None:
|
||||
if author is not None:
|
||||
if replace or self.__atom_author is None:
|
||||
self.__atom_author = []
|
||||
self.__atom_author += ensure_format(author,
|
||||
set(['name', 'email', 'uri']), set(['name']))
|
||||
set(['name', 'email', 'uri']),
|
||||
set(['name']))
|
||||
self.__rss_author = []
|
||||
for a in self.__atom_author:
|
||||
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
|
||||
|
||||
|
||||
def content(self, content=None, src=None, type=None):
|
||||
'''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
|
||||
if there is no alternate link, and should be provided if there is no
|
||||
summary. If the content is set (not linked) it will also set
|
||||
complete content of the entry. Content must be provided for ATOM
|
||||
entries if there is no alternate link, and should be provided if there
|
||||
is no summary. If the content is set (not linked) it will also set
|
||||
rss:description.
|
||||
|
||||
:param content: The content of the feed entry.
|
||||
|
@ -352,20 +352,20 @@ class FeedEntry(object):
|
|||
:param type: If type is CDATA content would not be escaped.
|
||||
:returns: Content element of the entry.
|
||||
'''
|
||||
if not src is None:
|
||||
if src is not None:
|
||||
self.__atom_content = {'src': src}
|
||||
elif not content is None:
|
||||
elif content is not None:
|
||||
self.__atom_content = {'content': content}
|
||||
self.__rss_content = {'content': content}
|
||||
if not type is None:
|
||||
if type is not None:
|
||||
self.__atom_content['type'] = type
|
||||
self.__rss_content['type'] = type
|
||||
return self.__atom_content
|
||||
|
||||
|
||||
def link(self, link=None, replace=False, **kwargs):
|
||||
'''Get or set link data. An link element is a dict with the fields href,
|
||||
rel, type, hreflang, title, and length. Href is mandatory for ATOM.
|
||||
'''Get or set link data. An link element is a dict with the fields
|
||||
href, rel, type, hreflang, title, and length. Href is mandatory for
|
||||
ATOM.
|
||||
|
||||
This method can be called with:
|
||||
- the fields of a link as keyword arguments
|
||||
|
@ -379,8 +379,8 @@ class FeedEntry(object):
|
|||
or one of the following predefined values (default=alternate):
|
||||
|
||||
- *alternate* an alternate representation of the entry or feed, for
|
||||
example a permalink to the html version of the entry, or the front
|
||||
page of the weblog.
|
||||
example a permalink to the html version of the entry, or the
|
||||
front page of the weblog.
|
||||
- *enclosure* a related resource which is potentially large in size
|
||||
and might require special handling, for example an audio or video
|
||||
recording.
|
||||
|
@ -397,9 +397,9 @@ class FeedEntry(object):
|
|||
RSS only supports one link with nothing but a URL. So for the RSS link
|
||||
element the last link with rel=alternate is used.
|
||||
|
||||
RSS also supports one enclusure element per entry which is covered by the
|
||||
link element in ATOM feed entries. So for the RSS enclusure element the
|
||||
last link with rel=enclosure is used.
|
||||
RSS also supports one enclusure element per entry which is covered by
|
||||
the link element in ATOM feed entries. So for the RSS enclusure element
|
||||
the last link with rel=enclosure is used.
|
||||
|
||||
:param link: Dict or list of dicts with data.
|
||||
:param replace: Add or replace old data.
|
||||
|
@ -407,10 +407,11 @@ class FeedEntry(object):
|
|||
'''
|
||||
if link is None and kwargs:
|
||||
link = kwargs
|
||||
if not link is None:
|
||||
if link is not None:
|
||||
if replace or self.__atom_link is None:
|
||||
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': ['alternate', 'enclosure', 'related', 'self', 'via']},
|
||||
|
@ -426,40 +427,39 @@ class FeedEntry(object):
|
|||
# return the set with more information (atom)
|
||||
return self.__atom_link
|
||||
|
||||
|
||||
def summary(self, summary=None):
|
||||
'''Get or set the summary element of an entry which conveys a short
|
||||
summary, abstract, or excerpt of the entry. Summary is an ATOM only
|
||||
element and should be provided if there either is no content provided for
|
||||
the entry, or that content is not inline (i.e., contains a src
|
||||
attribute), or if the content is encoded in base64.
|
||||
This method will also set the rss:description field if it wasn't
|
||||
previously set or contains the old value of summary.
|
||||
element and should be provided if there either is no content provided
|
||||
for the entry, or that content is not inline (i.e., contains a src
|
||||
attribute), or if the content is encoded in base64. This method will
|
||||
also set the rss:description field if it wasn't previously set or
|
||||
contains the old value of summary.
|
||||
|
||||
:param summary: Summary of the entries contents.
|
||||
:returns: Summary of the entries contents.
|
||||
'''
|
||||
if not summary is None:
|
||||
# Replace the RSS description with the summary if it was the summary
|
||||
# before. Not if is the description.
|
||||
if summary is not None:
|
||||
# Replace the RSS description with the summary if it was the
|
||||
# summary before. Not if is the description.
|
||||
if not self.__rss_description or \
|
||||
self.__rss_description == self.__atom_summary:
|
||||
self.__rss_description = summary
|
||||
self.__atom_summary = summary
|
||||
return self.__atom_summary
|
||||
|
||||
|
||||
def description(self, description=None, isSummary=False):
|
||||
'''Get or set the description value which is the item synopsis.
|
||||
Description is an RSS only element. For ATOM feeds it is split in summary
|
||||
and content. The isSummary parameter can be used to control which ATOM
|
||||
value is set when setting description.
|
||||
Description is an RSS only element. For ATOM feeds it is split in
|
||||
summary and content. The isSummary parameter can be used to control
|
||||
which ATOM value is set when setting description.
|
||||
|
||||
:param description: Description of the entry.
|
||||
:param isSummary: If the description should be used as content or summary.
|
||||
:param isSummary: If the description should be used as content or
|
||||
summary.
|
||||
:returns: The entries description.
|
||||
'''
|
||||
if not description is None:
|
||||
if description is not None:
|
||||
self.__rss_description = description
|
||||
if isSummary:
|
||||
self.__atom_summary = description
|
||||
|
@ -467,7 +467,6 @@ class FeedEntry(object):
|
|||
self.__atom_content = description
|
||||
return self.__rss_description
|
||||
|
||||
|
||||
def category(self, category=None, replace=False, **kwargs):
|
||||
'''Get or set categories that the entry belongs to.
|
||||
|
||||
|
@ -481,8 +480,8 @@ class FeedEntry(object):
|
|||
- *scheme* identifies the categorization scheme via a URI.
|
||||
- *label* provides a human-readable label for display
|
||||
|
||||
If a label is present it is used for the RSS feeds. Otherwise the term is
|
||||
used. The scheme is used for the domain attribute in RSS.
|
||||
If a label is present it is used for the RSS feeds. Otherwise the term
|
||||
is used. The scheme is used for the domain attribute in RSS.
|
||||
|
||||
:param category: Dict or list of dicts with data.
|
||||
:param replace: Add or replace old data.
|
||||
|
@ -490,7 +489,7 @@ class FeedEntry(object):
|
|||
'''
|
||||
if category is None and kwargs:
|
||||
category = kwargs
|
||||
if not category is None:
|
||||
if category is not None:
|
||||
if replace or self.__atom_category is None:
|
||||
self.__atom_category = []
|
||||
self.__atom_category += ensure_format(
|
||||
|
@ -503,13 +502,12 @@ class FeedEntry(object):
|
|||
self.__rss_category = []
|
||||
for cat in self.__atom_category:
|
||||
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'):
|
||||
rss_cat['domain'] = cat['scheme']
|
||||
self.__rss_category.append(rss_cat)
|
||||
return self.__atom_category
|
||||
|
||||
|
||||
def contributor(self, contributor=None, replace=False, **kwargs):
|
||||
'''Get or set the contributor data of the feed. This is an ATOM only
|
||||
value.
|
||||
|
@ -524,32 +522,32 @@ class FeedEntry(object):
|
|||
- *uri* contains a home page for the person.
|
||||
- *email* contains an email address for the person.
|
||||
|
||||
:param contributor: Dictionary or list of dictionaries with contributor data.
|
||||
:param contributor: Dictionary or list of dictionaries with contributor
|
||||
data.
|
||||
:param replace: Add or replace old data.
|
||||
:returns: List of contributors as dictionaries.
|
||||
'''
|
||||
if contributor is None and kwargs:
|
||||
contributor = kwargs
|
||||
if not contributor is None:
|
||||
if contributor is not None:
|
||||
if replace or self.__atom_contributor is None:
|
||||
self.__atom_contributor = []
|
||||
self.__atom_contributor += ensure_format( contributor,
|
||||
set(['name', 'email', 'uri']), set(['name']))
|
||||
self.__atom_contributor += ensure_format(
|
||||
contributor, set(['name', 'email', 'uri']), set(['name']))
|
||||
return self.__atom_contributor
|
||||
|
||||
|
||||
def published(self, published=None):
|
||||
'''Set or get the published value which contains the time of the initial
|
||||
creation or first availability of the entry.
|
||||
|
||||
The value can either be a string which will automatically be parsed or a
|
||||
datetime.datetime object. In any case it is necessary that the value
|
||||
The value can either be a string which will automatically be parsed or
|
||||
a datetime.datetime object. In any case it is necessary that the value
|
||||
include timezone information.
|
||||
|
||||
:param published: The creation date.
|
||||
:returns: Creation date as datetime.datetime
|
||||
'''
|
||||
if not published is None:
|
||||
if published is not None:
|
||||
if isinstance(published, string_types):
|
||||
published = dateutil.parser.parse(published)
|
||||
if not isinstance(published, datetime):
|
||||
|
@ -561,71 +559,65 @@ class FeedEntry(object):
|
|||
|
||||
return self.__atom_published
|
||||
|
||||
|
||||
def pubdate(self, pubDate=None):
|
||||
'''Get or set the pubDate of the entry which indicates when the entry was
|
||||
published. This method is just another name for the published(...)
|
||||
'''Get or set the pubDate of the entry which indicates when the entry
|
||||
was published. This method is just another name for the published(...)
|
||||
method.
|
||||
'''
|
||||
return self.published(pubDate)
|
||||
|
||||
|
||||
def rights(self, rights=None):
|
||||
'''Get or set the rights value of the entry which conveys information
|
||||
about rights, e.g. copyrights, held in and over the entry. This ATOM value
|
||||
will also set rss:copyright.
|
||||
about rights, e.g. copyrights, held in and over the entry. This ATOM
|
||||
value will also set rss:copyright.
|
||||
|
||||
:param rights: Rights information of the feed.
|
||||
:returns: Rights information of the feed.
|
||||
'''
|
||||
if not rights is None:
|
||||
if rights is not None:
|
||||
self.__atom_rights = rights
|
||||
return self.__atom_rights
|
||||
|
||||
|
||||
def comments(self, comments=None):
|
||||
'''Get or set the the value of comments which is the url of the comments
|
||||
page for the item. This is a RSS only value.
|
||||
'''Get or set the the value of comments which is the url of the
|
||||
comments page for the item. This is a RSS only value.
|
||||
|
||||
:param comments: URL to the comments page.
|
||||
:returns: URL to the comments page.
|
||||
'''
|
||||
if not comments is None:
|
||||
if comments is not None:
|
||||
self.__rss_comments = comments
|
||||
return self.__rss_comments
|
||||
|
||||
|
||||
def enclosure(self, url=None, length=None, type=None):
|
||||
'''Get or set the value of enclosure which describes a media object that
|
||||
is attached to the item. This is a RSS only value which is represented by
|
||||
link(rel=enclosure) in ATOM. ATOM feeds can furthermore contain several
|
||||
enclosures while RSS may contain only one. That is why this method, if
|
||||
repeatedly called, will add more than one enclosures to the feed.
|
||||
However, only the last one is used for RSS.
|
||||
'''Get or set the value of enclosure which describes a media object
|
||||
that is attached to the item. This is a RSS only value which is
|
||||
represented by link(rel=enclosure) in ATOM. ATOM feeds can furthermore
|
||||
contain several enclosures while RSS may contain only one. That is why
|
||||
this method, if repeatedly called, will add more than one enclosures to
|
||||
the feed. However, only the last one is used for RSS.
|
||||
|
||||
:param url: URL of the media object.
|
||||
:param length: Size of the media in bytes.
|
||||
:param type: Mimetype of the linked media.
|
||||
:returns: Data of the enclosure element.
|
||||
'''
|
||||
if not url is None:
|
||||
if url is not None:
|
||||
self.link(href=url, rel='enclosure', type=type, length=length)
|
||||
return self.__rss_enclosure
|
||||
|
||||
|
||||
def ttl(self, ttl=None):
|
||||
'''Get or set the ttl value. It is an RSS only element. ttl stands for
|
||||
time to live. It's a number of minutes that indicates how long a channel
|
||||
can be cached before refreshing from the source.
|
||||
time to live. It's a number of minutes that indicates how long a
|
||||
channel can be cached before refreshing from the source.
|
||||
|
||||
:param ttl: Integer value representing the time to live.
|
||||
:returns: Time to live of of the entry.
|
||||
'''
|
||||
if not ttl is None:
|
||||
if ttl is not None:
|
||||
self.__rss_ttl = int(ttl)
|
||||
return self.__rss_ttl
|
||||
|
||||
|
||||
def load_extension(self, name, atom=True, rss=True):
|
||||
'''Load a specific extension by name.
|
||||
|
||||
|
@ -651,7 +643,6 @@ class FeedEntry(object):
|
|||
ext = getattr(extmod, extname)
|
||||
self.register_extension(name, ext, atom, rss)
|
||||
|
||||
|
||||
def register_extension(self, namespace, extension_class_entry=None,
|
||||
atom=True, rss=True):
|
||||
'''Register a specific extension by classes to a namespace.
|
||||
|
|
|
@ -16,7 +16,8 @@ class BaseExtension(object):
|
|||
'''Basic FeedGenerator extension.
|
||||
'''
|
||||
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()
|
||||
|
||||
def extend_rss(self, feed):
|
||||
|
@ -27,7 +28,6 @@ class BaseExtension(object):
|
|||
'''
|
||||
return feed
|
||||
|
||||
|
||||
def extend_atom(self, feed):
|
||||
'''Extend an ATOM feed xml structure containing all previously set
|
||||
fields.
|
||||
|
|
|
@ -8,20 +8,19 @@
|
|||
Descriptions partly taken from
|
||||
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.
|
||||
'''
|
||||
|
||||
from lxml import etree
|
||||
from feedgen.ext.base import BaseExtension, BaseEntryExtension
|
||||
from feedgen.ext.base import BaseExtension
|
||||
|
||||
|
||||
class DcBaseExtension(BaseExtension):
|
||||
'''Dublin Core Elements extension for podcasts.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self):
|
||||
# http://dublincore.org/documents/usageguide/elements.shtml
|
||||
# http://dublincore.org/documents/dces/
|
||||
|
@ -52,15 +51,16 @@ class DcBaseExtension(BaseExtension):
|
|||
'''
|
||||
DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/'
|
||||
|
||||
for elem in ['contributor', 'coverage', 'creator', 'date', 'description',
|
||||
'language', 'publisher', 'relation', 'rights', 'source', 'subject',
|
||||
'title', 'type', 'format', 'identifier']:
|
||||
for elem in ['contributor', 'coverage', 'creator', 'date',
|
||||
'description', 'language', 'publisher', 'relation',
|
||||
'rights', 'source', 'subject', 'title', 'type', 'format',
|
||||
'identifier']:
|
||||
if hasattr(self, '_dcelem_%s' % elem):
|
||||
for val in getattr(self, '_dcelem_%s' % elem) or []:
|
||||
node = etree.SubElement(xml_elem, '{%s}%s' % (DCELEMENTS_NS, elem))
|
||||
node = etree.SubElement(xml_elem,
|
||||
'{%s}%s' % (DCELEMENTS_NS, elem))
|
||||
node.text = val
|
||||
|
||||
|
||||
def extend_atom(self, atom_feed):
|
||||
'''Extend an Atom feed with the set DC fields.
|
||||
|
||||
|
@ -72,8 +72,6 @@ class DcBaseExtension(BaseExtension):
|
|||
|
||||
return atom_feed
|
||||
|
||||
|
||||
|
||||
def extend_rss(self, rss_feed):
|
||||
'''Extend a RSS feed with the set DC fields.
|
||||
|
||||
|
@ -85,7 +83,6 @@ class DcBaseExtension(BaseExtension):
|
|||
|
||||
return rss_feed
|
||||
|
||||
|
||||
def dc_contributor(self, contributor=None, replace=False):
|
||||
'''Get or set the dc:contributor which is an entity responsible for
|
||||
making contributions to the resource.
|
||||
|
@ -97,7 +94,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set contributors (deault: False).
|
||||
:returns: List of contributors.
|
||||
'''
|
||||
if not contributor is None:
|
||||
if contributor is not None:
|
||||
if not isinstance(contributor, list):
|
||||
contributor = [contributor]
|
||||
if replace or not self._dcelem_contributor:
|
||||
|
@ -105,28 +102,28 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_contributor += contributor
|
||||
return self._dcelem_contributor
|
||||
|
||||
|
||||
def dc_coverage(self, coverage=None, replace=True):
|
||||
'''Get or set the dc:coverage which indicated the spatial or temporal
|
||||
topic of the resource, the spatial applicability of the resource, or the
|
||||
jurisdiction under which the resource is relevant.
|
||||
topic of the resource, the spatial applicability of the resource, or
|
||||
the jurisdiction under which the resource is relevant.
|
||||
|
||||
Spatial topic and spatial applicability may be a named place or a
|
||||
location specified by its geographic coordinates. Temporal topic may be a
|
||||
named period, date, or date range. A jurisdiction may be a named
|
||||
location specified by its geographic coordinates. Temporal topic may be
|
||||
a named period, date, or date range. A jurisdiction may be a named
|
||||
administrative entity or a geographic place to which the resource
|
||||
applies. Recommended best practice is to use a controlled vocabulary such
|
||||
as the Thesaurus of Geographic Names [TGN]. Where appropriate, named
|
||||
places or time periods can be used in preference to numeric identifiers
|
||||
such as sets of coordinates or date ranges.
|
||||
applies. Recommended best practice is to use a controlled vocabulary
|
||||
such as the Thesaurus of Geographic Names [TGN]. Where appropriate,
|
||||
named places or time periods can be used in preference to numeric
|
||||
identifiers such as sets of coordinates or date ranges.
|
||||
|
||||
References: [TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html
|
||||
References:
|
||||
[TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html
|
||||
|
||||
:param coverage: Coverage of the feed.
|
||||
:param replace: Replace already set coverage (default: True).
|
||||
:returns: Coverage of the feed.
|
||||
'''
|
||||
if not coverage is None:
|
||||
if coverage is not None:
|
||||
if not isinstance(coverage, list):
|
||||
coverage = [coverage]
|
||||
if replace or not self._dcelem_coverage:
|
||||
|
@ -134,10 +131,9 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_coverage = coverage
|
||||
return self._dcelem_coverage
|
||||
|
||||
|
||||
def dc_creator(self, creator=None, replace=False):
|
||||
'''Get or set the dc:creator which is an entity primarily responsible for
|
||||
making the resource.
|
||||
'''Get or set the dc:creator which is an entity primarily responsible
|
||||
for making the resource.
|
||||
|
||||
For more information see:
|
||||
http://dublincore.org/documents/dcmi-terms/#elements-creator
|
||||
|
@ -146,7 +142,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set creators (deault: False).
|
||||
:returns: List of creators.
|
||||
'''
|
||||
if not creator is None:
|
||||
if creator is not None:
|
||||
if not isinstance(creator, list):
|
||||
creator = [creator]
|
||||
if replace or not self._dcelem_creator:
|
||||
|
@ -154,7 +150,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_creator += creator
|
||||
return self._dcelem_creator
|
||||
|
||||
|
||||
def dc_date(self, date=None, replace=True):
|
||||
'''Get or set the dc:date which describes a point or period of time
|
||||
associated with an event in the lifecycle of the resource.
|
||||
|
@ -166,7 +161,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set dates (deault: True).
|
||||
:returns: List of dates.
|
||||
'''
|
||||
if not date is None:
|
||||
if date is not None:
|
||||
if not isinstance(date, list):
|
||||
date = [date]
|
||||
if replace or not self._dcelem_date:
|
||||
|
@ -174,7 +169,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_date += date
|
||||
return self._dcelem_date
|
||||
|
||||
|
||||
def dc_description(self, description=None, replace=True):
|
||||
'''Get or set the dc:description which is an account of the resource.
|
||||
|
||||
|
@ -185,7 +179,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set descriptions (deault: True).
|
||||
:returns: List of descriptions.
|
||||
'''
|
||||
if not description is None:
|
||||
if description is not None:
|
||||
if not isinstance(description, list):
|
||||
description = [description]
|
||||
if replace or not self._dcelem_description:
|
||||
|
@ -193,7 +187,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_description += description
|
||||
return self._dcelem_description
|
||||
|
||||
|
||||
def dc_format(self, format=None, replace=True):
|
||||
'''Get or set the dc:format which describes the file format, physical
|
||||
medium, or dimensions of the resource.
|
||||
|
@ -205,7 +198,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set format (deault: True).
|
||||
:returns: Format of the resource.
|
||||
'''
|
||||
if not format is None:
|
||||
if format is not None:
|
||||
if not isinstance(format, list):
|
||||
format = [format]
|
||||
if replace or not self._dcelem_format:
|
||||
|
@ -213,10 +206,9 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_format += format
|
||||
return self._dcelem_format
|
||||
|
||||
|
||||
def dc_identifier(self, identifier=None, replace=True):
|
||||
'''Get or set the dc:identifier which should be an unambiguous reference
|
||||
to the resource within a given context.
|
||||
'''Get or set the dc:identifier which should be an unambiguous
|
||||
reference to the resource within a given context.
|
||||
|
||||
For more inidentifierion see:
|
||||
http://dublincore.org/documents/dcmi-terms/#elements-identifier
|
||||
|
@ -225,16 +217,16 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set identifier (deault: True).
|
||||
:returns: Identifiers of the resource.
|
||||
'''
|
||||
if not identifier is None:
|
||||
if identifier is not None:
|
||||
if not isinstance(identifier, list):
|
||||
identifier = [identifier]
|
||||
if replace or not self._dcelem_identifier:
|
||||
self._dcelem_identifier = []
|
||||
self._dcelem_identifier += identifier
|
||||
|
||||
|
||||
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:
|
||||
http://dublincore.org/documents/dcmi-terms/#elements-language
|
||||
|
@ -243,7 +235,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set languages (deault: True).
|
||||
:returns: List of languages.
|
||||
'''
|
||||
if not language is None:
|
||||
if language is not None:
|
||||
if not isinstance(language, list):
|
||||
language = [language]
|
||||
if replace or not self._dcelem_language:
|
||||
|
@ -251,10 +243,9 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_language += language
|
||||
return self._dcelem_language
|
||||
|
||||
|
||||
def dc_publisher(self, publisher=None, replace=False):
|
||||
'''Get or set the dc:publisher which is an entity responsible for making
|
||||
the resource available.
|
||||
'''Get or set the dc:publisher which is an entity responsible for
|
||||
making the resource available.
|
||||
|
||||
For more information see:
|
||||
http://dublincore.org/documents/dcmi-terms/#elements-publisher
|
||||
|
@ -263,7 +254,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set publishers (deault: False).
|
||||
:returns: List of publishers.
|
||||
'''
|
||||
if not publisher is None:
|
||||
if publisher is not None:
|
||||
if not isinstance(publisher, list):
|
||||
publisher = [publisher]
|
||||
if replace or not self._dcelem_publisher:
|
||||
|
@ -271,7 +262,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_publisher += publisher
|
||||
return self._dcelem_publisher
|
||||
|
||||
|
||||
def dc_relation(self, relation=None, replace=False):
|
||||
'''Get or set the dc:relation which describes a related ressource.
|
||||
|
||||
|
@ -282,7 +272,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set relations (deault: False).
|
||||
:returns: List of relations.
|
||||
'''
|
||||
if not relation is None:
|
||||
if relation is not None:
|
||||
if not isinstance(relation, list):
|
||||
relation = [relation]
|
||||
if replace or not self._dcelem_relation:
|
||||
|
@ -290,7 +280,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_relation += relation
|
||||
return self._dcelem_relation
|
||||
|
||||
|
||||
def dc_rights(self, rights=None, replace=False):
|
||||
'''Get or set the dc:rights which may contain information about rights
|
||||
held in and over the resource.
|
||||
|
@ -302,7 +291,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set rightss (deault: False).
|
||||
:returns: List of rights information.
|
||||
'''
|
||||
if not rights is None:
|
||||
if rights is not None:
|
||||
if not isinstance(rights, list):
|
||||
rights = [rights]
|
||||
if replace or not self._dcelem_rights:
|
||||
|
@ -310,15 +299,14 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_rights += rights
|
||||
return self._dcelem_rights
|
||||
|
||||
|
||||
def dc_source(self, source=None, replace=False):
|
||||
'''Get or set the dc:source which is a related resource from which the
|
||||
described resource is derived.
|
||||
|
||||
The described resource may be derived from the related resource in whole
|
||||
or in part. Recommended best practice is to identify the related resource
|
||||
by means of a string conforming to a formal identification system.
|
||||
|
||||
The described resource may be derived from the related resource in
|
||||
whole or in part. Recommended best practice is to identify the related
|
||||
resource by means of a string conforming to a formal identification
|
||||
system.
|
||||
|
||||
For more information see:
|
||||
http://dublincore.org/documents/dcmi-terms/#elements-source
|
||||
|
@ -327,7 +315,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set sources (deault: False).
|
||||
:returns: List of sources.
|
||||
'''
|
||||
if not source is None:
|
||||
if source is not None:
|
||||
if not isinstance(source, list):
|
||||
source = [source]
|
||||
if replace or not self._dcelem_source:
|
||||
|
@ -335,7 +323,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_source += source
|
||||
return self._dcelem_source
|
||||
|
||||
|
||||
def dc_subject(self, subject=None, replace=False):
|
||||
'''Get or set the dc:subject which describes the topic of the resource.
|
||||
|
||||
|
@ -346,7 +333,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set subjects (deault: False).
|
||||
:returns: List of subjects.
|
||||
'''
|
||||
if not subject is None:
|
||||
if subject is not None:
|
||||
if not isinstance(subject, list):
|
||||
subject = [subject]
|
||||
if replace or not self._dcelem_subject:
|
||||
|
@ -354,7 +341,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_subject += subject
|
||||
return self._dcelem_subject
|
||||
|
||||
|
||||
def dc_title(self, title=None, replace=True):
|
||||
'''Get or set the dc:title which is a name given to the resource.
|
||||
|
||||
|
@ -365,7 +351,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set titles (deault: False).
|
||||
:returns: List of titles.
|
||||
'''
|
||||
if not title is None:
|
||||
if title is not None:
|
||||
if not isinstance(title, list):
|
||||
title = [title]
|
||||
if replace or not self._dcelem_title:
|
||||
|
@ -373,7 +359,6 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_title += title
|
||||
return self._dcelem_title
|
||||
|
||||
|
||||
def dc_type(self, type=None, replace=False):
|
||||
'''Get or set the dc:type which describes the nature or genre of the
|
||||
resource.
|
||||
|
@ -385,7 +370,7 @@ class DcBaseExtension(BaseExtension):
|
|||
:param replace: Replace alredy set types (deault: False).
|
||||
:returns: List of types.
|
||||
'''
|
||||
if not type is None:
|
||||
if type is not None:
|
||||
if not isinstance(type, list):
|
||||
type = [type]
|
||||
if replace or not self._dcelem_type:
|
||||
|
@ -393,10 +378,12 @@ class DcBaseExtension(BaseExtension):
|
|||
self._dcelem_type += type
|
||||
return self._dcelem_type
|
||||
|
||||
|
||||
class DcExtension(DcBaseExtension):
|
||||
'''Dublin Core Elements extension for podcasts.
|
||||
'''
|
||||
|
||||
|
||||
class DcEntryExtension(DcBaseExtension):
|
||||
'''Dublin Core Elements extension for podcasts.
|
||||
'''
|
||||
|
|
|
@ -20,9 +20,8 @@ class PodcastExtension(BaseExtension):
|
|||
'''FeedGenerator extension for podcasts.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self):
|
||||
## ITunes tags
|
||||
# ITunes tags
|
||||
# http://www.apple.com/itunes/podcasts/specs.html#rss
|
||||
self.__itunes_author = None
|
||||
self.__itunes_block = None
|
||||
|
@ -35,11 +34,9 @@ class PodcastExtension(BaseExtension):
|
|||
self.__itunes_subtitle = None
|
||||
self.__itunes_summary = None
|
||||
|
||||
|
||||
def extend_ns(self):
|
||||
return {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
|
||||
|
||||
|
||||
def extend_rss(self, rss_feed):
|
||||
'''Extend an RSS feed root with set itunes fields.
|
||||
|
||||
|
@ -52,20 +49,23 @@ class PodcastExtension(BaseExtension):
|
|||
author = etree.SubElement(channel, '{%s}author' % ITUNES_NS)
|
||||
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.text = 'yes' if self.__itunes_block else 'no'
|
||||
|
||||
for c in self.__itunes_category or []:
|
||||
if not c.get('cat'):
|
||||
continue
|
||||
category = channel.find('{%s}category[@text="%s"]' % (ITUNES_NS,c.get('cat')))
|
||||
if category == None:
|
||||
category = etree.SubElement(channel, '{%s}category' % ITUNES_NS)
|
||||
category = channel.find(
|
||||
'{%s}category[@text="%s"]' % (ITUNES_NS, c.get('cat')))
|
||||
if category is None:
|
||||
category = etree.SubElement(channel,
|
||||
'{%s}category' % ITUNES_NS)
|
||||
category.attrib['text'] = c.get('cat')
|
||||
|
||||
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')
|
||||
|
||||
if self.__itunes_image:
|
||||
|
@ -81,7 +81,8 @@ class PodcastExtension(BaseExtension):
|
|||
complete.text = self.__itunes_complete
|
||||
|
||||
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
|
||||
|
||||
if self.__itunes_owner:
|
||||
|
@ -101,29 +102,27 @@ class PodcastExtension(BaseExtension):
|
|||
|
||||
return rss_feed
|
||||
|
||||
|
||||
def itunes_author(self, itunes_author=None):
|
||||
'''Get or set the itunes:author. The content of this tag is shown in the
|
||||
Artist column in iTunes. If the tag is not present, iTunes uses the
|
||||
'''Get or set the itunes:author. The content of this tag is shown in
|
||||
the Artist column in iTunes. If the tag is not present, iTunes uses the
|
||||
contents of the <author> tag. If <itunes:author> is not present at the
|
||||
feed level, iTunes will use the contents of <managingEditor>.
|
||||
|
||||
:param itunes_author: 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
|
||||
return self.__itunes_author
|
||||
|
||||
|
||||
def itunes_block(self, itunes_block=None):
|
||||
'''Get or set the ITunes block attribute. Use this to prevent the entire
|
||||
podcast from appearing in the iTunes podcast directory.
|
||||
'''Get or set the ITunes block attribute. Use this to prevent the
|
||||
entire podcast from appearing in the iTunes podcast directory.
|
||||
|
||||
:param itunes_block: Block the podcast.
|
||||
:returns: If the podcast is blocked.
|
||||
'''
|
||||
if not itunes_block is None:
|
||||
if itunes_block is not None:
|
||||
self.__itunes_block = itunes_block
|
||||
return self.__itunes_block
|
||||
|
||||
|
@ -167,14 +166,14 @@ class PodcastExtension(BaseExtension):
|
|||
|
||||
---
|
||||
|
||||
**Important note about deprecated parameter syntax:** Old version of the
|
||||
feedgen did only support one category plus one subcategory which would be
|
||||
passed to this ducntion as first two parameters. For compatibility
|
||||
reasons, this still works but should not be used any may be removed at
|
||||
any time.
|
||||
**Important note about deprecated parameter syntax:** Old version of
|
||||
the feedgen did only support one category plus one subcategory which
|
||||
would be passed to this ducntion as first two parameters. For
|
||||
compatibility reasons, this still works but should not be used any may
|
||||
be removed at any time.
|
||||
'''
|
||||
# Ensure old API still works for now. Note that the API is deprecated and
|
||||
# this fallback may be removed at any time.
|
||||
# Ensure old API still works for now. Note that the API is deprecated
|
||||
# and this fallback may be removed at any time.
|
||||
if isinstance(itunes_category, string_types):
|
||||
itunes_category = {'cat': itunes_category}
|
||||
if replace:
|
||||
|
@ -182,21 +181,21 @@ class PodcastExtension(BaseExtension):
|
|||
replace = True
|
||||
if itunes_category is None and kwargs:
|
||||
itunes_category = kwargs
|
||||
if not itunes_category is None:
|
||||
if itunes_category is not None:
|
||||
if replace or self.__itunes_category is None:
|
||||
self.__itunes_category = []
|
||||
self.__itunes_category += ensure_format(itunes_category,
|
||||
set(['cat', 'sub']), set(['cat']))
|
||||
set(['cat', 'sub']),
|
||||
set(['cat']))
|
||||
return self.__itunes_category
|
||||
|
||||
|
||||
def itunes_image(self, itunes_image=None):
|
||||
'''Get or set the image for the podcast. This tag specifies the artwork
|
||||
for your podcast. Put the URL to the image in the href attribute. iTunes
|
||||
prefers square .jpg images that are at least 1400x1400 pixels, which is
|
||||
different from what is specified for the standard RSS image tag. In order
|
||||
for a podcast to be eligible for an iTunes Store feature, the
|
||||
accompanying image must be at least 1400x1400 pixels.
|
||||
for your podcast. Put the URL to the image in the href attribute.
|
||||
iTunes prefers square .jpg images that are at least 1400x1400 pixels,
|
||||
which is different from what is specified for the standard RSS image
|
||||
tag. In order for a podcast to be eligible for an iTunes Store feature,
|
||||
the accompanying image must be at least 1400x1400 pixels.
|
||||
|
||||
iTunes supports images in JPEG and PNG formats with an RGB color space
|
||||
(CMYK is not supported). The URL must end in ".jpg" or ".png". If the
|
||||
|
@ -204,43 +203,43 @@ class PodcastExtension(BaseExtension):
|
|||
RSS image tag.
|
||||
|
||||
If you change your podcast’s image, also change the file’s name. iTunes
|
||||
may not change the image if it checks your feed and the image URL is the
|
||||
same. The server hosting your cover art image must allow HTTP head
|
||||
may not change the image if it checks your feed and the image URL is
|
||||
the same. The server hosting your cover art image must allow HTTP head
|
||||
requests for iTS to be able to automatically update your cover art.
|
||||
|
||||
:param itunes_image: Image of the podcast.
|
||||
:returns: Image of the podcast.
|
||||
'''
|
||||
if not itunes_image is None:
|
||||
if not ( itunes_image.endswith('.jpg') or itunes_image.endswith('.png') ):
|
||||
ValueError('Image file must be png or jpg')
|
||||
if itunes_image is not None:
|
||||
if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
|
||||
self.__itunes_image = itunes_image
|
||||
else:
|
||||
ValueError('Image file must be png or jpg')
|
||||
return self.__itunes_image
|
||||
|
||||
|
||||
def itunes_explicit(self, itunes_explicit=None):
|
||||
'''Get or the the itunes:explicit value of the podcast. This tag should
|
||||
be used to indicate whether your podcast contains explicit material. The
|
||||
three values for this tag are "yes", "no", and "clean".
|
||||
be used to indicate whether your podcast contains explicit material.
|
||||
The three values for this tag are "yes", "no", and "clean".
|
||||
|
||||
If you populate this tag with "yes", an "explicit" parental advisory
|
||||
graphic will appear next to your podcast artwork on the iTunes Store and
|
||||
in the Name column in iTunes. If the value is "clean", the parental
|
||||
graphic will appear next to your podcast artwork on the iTunes Store
|
||||
and in the Name column in iTunes. If the value is "clean", the parental
|
||||
advisory type is considered Clean, meaning that no explicit language or
|
||||
adult content is included anywhere in the episodes, and a "clean" graphic
|
||||
will appear. If the explicit tag is present and has any other value
|
||||
(e.g., "no"), you see no indicator — blank is the default advisory type.
|
||||
adult content is included anywhere in the episodes, and a "clean"
|
||||
graphic will appear. If the explicit tag is present and has any other
|
||||
value (e.g., "no"), you see no indicator — blank is the default
|
||||
advisory type.
|
||||
|
||||
:param itunes_explicit: If the podcast contains explicit material.
|
||||
:returns: If the podcast contains explicit material.
|
||||
'''
|
||||
if not itunes_explicit is None:
|
||||
if not itunes_explicit in ('', 'yes', 'no', 'clean'):
|
||||
if itunes_explicit is not None:
|
||||
if itunes_explicit not in ('', 'yes', 'no', 'clean'):
|
||||
raise ValueError('Invalid value for explicit tag')
|
||||
self.__itunes_explicit = itunes_explicit
|
||||
return self.__itunes_explicit
|
||||
|
||||
|
||||
def itunes_complete(self, itunes_complete=None):
|
||||
'''Get or set the itunes:complete value of the podcast. This tag can be
|
||||
used to indicate the completion of a podcast.
|
||||
|
@ -253,33 +252,31 @@ class PodcastExtension(BaseExtension):
|
|||
:param itunes_complete: If the podcast is complete.
|
||||
:returns: If the podcast is complete.
|
||||
'''
|
||||
if not itunes_complete is None:
|
||||
if not itunes_complete in ('yes', 'no', '', True, False):
|
||||
if itunes_complete is not None:
|
||||
if itunes_complete not in ('yes', 'no', '', True, False):
|
||||
raise ValueError('Invalid value for complete tag')
|
||||
if itunes_complete == True:
|
||||
if itunes_complete is True:
|
||||
itunes_complete = 'yes'
|
||||
if itunes_complete == False:
|
||||
if itunes_complete is False:
|
||||
itunes_complete = 'no'
|
||||
self.__itunes_complete = itunes_complete
|
||||
return self.__itunes_complete
|
||||
|
||||
|
||||
def itunes_new_feed_url(self, itunes_new_feed_url=None):
|
||||
'''Get or set the new-feed-url property of the podcast. This tag allows
|
||||
you to change the URL where the podcast feed is located
|
||||
|
||||
After adding the tag to your old feed, you should maintain the old feed
|
||||
for 48 hours before retiring it. At that point, iTunes will have updated
|
||||
the directory with the new feed URL.
|
||||
for 48 hours before retiring it. At that point, iTunes will have
|
||||
updated the directory with the new feed URL.
|
||||
|
||||
:param itunes_new_feed_url: New feed URL.
|
||||
:returns: New feed URL.
|
||||
'''
|
||||
if not itunes_new_feed_url is None:
|
||||
if itunes_new_feed_url is not None:
|
||||
self.__itunes_new_feed_url = itunes_new_feed_url
|
||||
return self.__itunes_new_feed_url
|
||||
|
||||
|
||||
def itunes_owner(self, name=None, email=None):
|
||||
'''Get or set the itunes:owner of the podcast. This tag contains
|
||||
information that will be used to contact the owner of the podcast for
|
||||
|
@ -289,7 +286,7 @@ class PodcastExtension(BaseExtension):
|
|||
:param itunes_owner: 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:
|
||||
self.__itunes_owner = {'name': name, 'email': email}
|
||||
elif not name and not email:
|
||||
|
@ -298,7 +295,6 @@ class PodcastExtension(BaseExtension):
|
|||
raise ValueError('Both name and email have to be set.')
|
||||
return self.__itunes_owner
|
||||
|
||||
|
||||
def itunes_subtitle(self, itunes_subtitle=None):
|
||||
'''Get or set the itunes:subtitle value for the podcast. The contents of
|
||||
this tag are shown in the Description column in iTunes. The subtitle
|
||||
|
@ -307,53 +303,57 @@ class PodcastExtension(BaseExtension):
|
|||
:param itunes_subtitle: 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
|
||||
return self.__itunes_subtitle
|
||||
|
||||
|
||||
def itunes_summary(self, itunes_summary=None):
|
||||
'''Get or set the itunes:summary value for the podcast. The contents of
|
||||
this tag are shown in a separate window that appears when the "circled i"
|
||||
in the Description column is clicked. It also appears on the iTunes page
|
||||
for your podcast. This field can be up to 4000 characters. If
|
||||
<itunes:summary> is not included, the contents of the <description> tag
|
||||
are used.
|
||||
this tag are shown in a separate window that appears when the "circled
|
||||
i" in the Description column is clicked. It also appears on the iTunes
|
||||
page for your podcast. This field can be up to 4000 characters. If
|
||||
`<itunes:summary>` is not included, the contents of the <description>
|
||||
tag are used.
|
||||
|
||||
:param itunes_summary: 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
|
||||
return self.__itunes_summary
|
||||
|
||||
|
||||
_itunes_categories = {
|
||||
'Arts': [ 'Design', 'Fashion & Beauty', 'Food', 'Literature',
|
||||
'Arts': [
|
||||
'Design', 'Fashion & Beauty', 'Food', 'Literature',
|
||||
'Performing Arts', 'Visual Arts'],
|
||||
'Business' : [ 'Business News', 'Careers', 'Investing',
|
||||
'Business': [
|
||||
'Business News', 'Careers', 'Investing',
|
||||
'Management & Marketing', 'Shopping'],
|
||||
'Comedy': [],
|
||||
'Education' : [ 'Education', 'Education Technology',
|
||||
'Higher Education', 'K-12', 'Language Courses', 'Training' ],
|
||||
'Games & Hobbies' : [ 'Automotive', 'Aviation', 'Hobbies',
|
||||
'Other Games', 'Video Games' ],
|
||||
'Government & Organizations' : [ 'Local', 'National', 'Non-Profit',
|
||||
'Regional' ],
|
||||
'Health' : [ 'Alternative Health', 'Fitness & Nutrition', 'Self-Help',
|
||||
'Education': [
|
||||
'Education', 'Education Technology', 'Higher Education',
|
||||
'K-12', 'Language Courses', 'Training'],
|
||||
'Games & Hobbies': [
|
||||
'Automotive', 'Aviation', 'Hobbies', 'Other Games',
|
||||
'Video Games'],
|
||||
'Government & Organizations': [
|
||||
'Local', 'National', 'Non-Profit', 'Regional'],
|
||||
'Health': [
|
||||
'Alternative Health', 'Fitness & Nutrition', 'Self-Help',
|
||||
'Sexuality'],
|
||||
'Kids & Family': [],
|
||||
'Music': [],
|
||||
'News & Politics': [],
|
||||
'Religion & Spirituality' : [ 'Buddhism', 'Christianity', 'Hinduism',
|
||||
'Islam', 'Judaism', 'Other', 'Spirituality' ],
|
||||
'Science & Medicine' : [ 'Medicine', 'Natural Sciences',
|
||||
'Social Sciences' ],
|
||||
'Society & Culture' : [ 'History', 'Personal Journals', 'Philosophy',
|
||||
'Religion & Spirituality': [
|
||||
'Buddhism', 'Christianity', 'Hinduism', 'Islam', 'Judaism',
|
||||
'Other', 'Spirituality'],
|
||||
'Science & Medicine': [
|
||||
'Medicine', 'Natural Sciences', 'Social Sciences'],
|
||||
'Society & Culture': [
|
||||
'History', 'Personal Journals', 'Philosophy',
|
||||
'Places & Travel'],
|
||||
'Sports & Recreation' : [ 'Amateur', 'College & High School',
|
||||
'Outdoor', 'Professional' ],
|
||||
'Technology' : [ 'Gadgets', 'Tech News', 'Podcasting',
|
||||
'Software How-To' ],
|
||||
'TV & Film' : []
|
||||
}
|
||||
'Sports & Recreation': [
|
||||
'Amateur', 'College & High School', 'Outdoor', 'Professional'],
|
||||
'Technology': [
|
||||
'Gadgets', 'Tech News', 'Podcasting', 'Software How-To'],
|
||||
'TV & Film': []}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
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.
|
||||
'''
|
||||
|
@ -18,9 +18,8 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
'''FeedEntry extension for podcasts.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self):
|
||||
## ITunes tags
|
||||
# ITunes tags
|
||||
# http://www.apple.com/itunes/podcasts/specs.html#rss
|
||||
self.__itunes_author = None
|
||||
self.__itunes_block = None
|
||||
|
@ -32,7 +31,6 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
self.__itunes_subtitle = None
|
||||
self.__itunes_summary = None
|
||||
|
||||
|
||||
def extend_rss(self, entry):
|
||||
'''Add additional fields to an RSS item.
|
||||
|
||||
|
@ -44,7 +42,7 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
author = etree.SubElement(entry, '{%s}author' % ITUNES_NS)
|
||||
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.text = 'yes' if self.__itunes_block else 'no'
|
||||
|
||||
|
@ -60,11 +58,15 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
explicit = etree.SubElement(entry, '{%s}explicit' % ITUNES_NS)
|
||||
explicit.text = self.__itunes_explicit
|
||||
|
||||
if not self.__itunes_is_closed_captioned is None:
|
||||
is_closed_captioned = etree.SubElement(entry, '{%s}isClosedCaptioned' % ITUNES_NS)
|
||||
is_closed_captioned.text = 'yes' if self.__itunes_is_closed_captioned else 'no'
|
||||
if self.__itunes_is_closed_captioned is not None:
|
||||
is_closed_captioned = etree.SubElement(
|
||||
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.text = str(self.__itunes_order)
|
||||
|
||||
|
@ -77,22 +79,20 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
summary.text = self.__itunes_summary
|
||||
return entry
|
||||
|
||||
|
||||
def itunes_author(self, itunes_author=None):
|
||||
'''Get or set the itunes:author of the podcast episode. The content of
|
||||
this tag is shown in the Artist column in iTunes. If the tag is not
|
||||
present, iTunes uses the contents of the <author> tag. If <itunes:author>
|
||||
is not present at the feed level, iTunes will use the contents of
|
||||
<managingEditor>.
|
||||
present, iTunes uses the contents of the <author> tag. If
|
||||
<itunes:author> is not present at the feed level, iTunes will use the
|
||||
contents of <managingEditor>.
|
||||
|
||||
:param itunes_author: 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
|
||||
return self.__itunes_author
|
||||
|
||||
|
||||
def itunes_block(self, itunes_block=None):
|
||||
'''Get or set the ITunes block attribute. Use this to prevent episodes
|
||||
from appearing in the iTunes podcast directory.
|
||||
|
@ -100,18 +100,18 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
:param itunes_block: Block podcast episodes.
|
||||
:returns: If the podcast episode is blocked.
|
||||
'''
|
||||
if not itunes_block is None:
|
||||
if itunes_block is not None:
|
||||
self.__itunes_block = itunes_block
|
||||
return self.__itunes_block
|
||||
|
||||
|
||||
def itunes_image(self, itunes_image=None):
|
||||
'''Get or set the image for the podcast episode. This tag specifies the
|
||||
artwork for your podcast. Put the URL to the image in the href attribute.
|
||||
iTunes prefers square .jpg images that are at least 1400x1400 pixels,
|
||||
which is different from what is specified for the standard RSS image tag.
|
||||
In order for a podcast to be eligible for an iTunes Store feature, the
|
||||
accompanying image must be at least 1400x1400 pixels.
|
||||
artwork for your podcast. Put the URL to the image in the href
|
||||
attribute. iTunes prefers square .jpg images that are at least
|
||||
1400x1400 pixels, which is different from what is specified for the
|
||||
standard RSS image tag. In order for a podcast to be eligible for an
|
||||
iTunes Store feature, the accompanying image must be at least 1400x1400
|
||||
pixels.
|
||||
|
||||
iTunes supports images in JPEG and PNG formats with an RGB color space
|
||||
(CMYK is not supported). The URL must end in ".jpg" or ".png". If the
|
||||
|
@ -119,35 +119,35 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
RSS image tag.
|
||||
|
||||
If you change your podcast’s image, also change the file’s name. iTunes
|
||||
may not change the image if it checks your feed and the image URL is the
|
||||
same. The server hosting your cover art image must allow HTTP head
|
||||
may not change the image if it checks your feed and the image URL is
|
||||
the same. The server hosting your cover art image must allow HTTP head
|
||||
requests for iTS to be able to automatically update your cover art.
|
||||
|
||||
:param itunes_image: Image of the podcast.
|
||||
:returns: Image of the podcast.
|
||||
'''
|
||||
if not itunes_image is None:
|
||||
if not ( itunes_image.endswith('.jpg') or itunes_image.endswith('.png') ):
|
||||
ValueError('Image file must be png or jpg')
|
||||
if itunes_image is not None:
|
||||
if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
|
||||
self.__itunes_image = itunes_image
|
||||
else:
|
||||
ValueError('Image file must be png or jpg')
|
||||
return self.__itunes_image
|
||||
|
||||
|
||||
def itunes_duration(self, itunes_duration=None):
|
||||
'''Get or set the duration of the podcast episode. The content of this
|
||||
tag is shown in the Time column in iTunes.
|
||||
|
||||
The tag can be formatted HH:MM:SS, H:MM:SS, MM:SS, or M:SS (H = hours,
|
||||
M = minutes, S = seconds). If an integer is provided (no colon present),
|
||||
the value is assumed to be in seconds. If one colon is present, the
|
||||
number to the left is assumed to be minutes, and the number to the right
|
||||
is assumed to be seconds. If more than two colons are present, the
|
||||
numbers farthest to the right are ignored.
|
||||
M = minutes, S = seconds). If an integer is provided (no colon
|
||||
present), the value is assumed to be in seconds. If one colon is
|
||||
present, the number to the left is assumed to be minutes, and the
|
||||
number to the right is assumed to be seconds. If more than two colons
|
||||
are present, the numbers farthest to the right are ignored.
|
||||
|
||||
:param itunes_duration: Duration of the podcast episode.
|
||||
:returns: Duration of the podcast episode.
|
||||
'''
|
||||
if not itunes_duration is None:
|
||||
if itunes_duration is not None:
|
||||
itunes_duration = str(itunes_duration)
|
||||
if len(itunes_duration.split(':')) > 3 or \
|
||||
itunes_duration.lstrip('0123456789:') != '':
|
||||
|
@ -155,65 +155,67 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
self.__itunes_duration = itunes_duration
|
||||
return self.itunes_duration
|
||||
|
||||
|
||||
def itunes_explicit(self, itunes_explicit=None):
|
||||
'''Get or the the itunes:explicit value of the podcast episode. This tag
|
||||
should be used to indicate whether your podcast episode contains explicit
|
||||
material. The three values for this tag are "yes", "no", and "clean".
|
||||
'''Get or the the itunes:explicit value of the podcast episode. This
|
||||
tag should be used to indicate whether your podcast episode contains
|
||||
explicit material. The three values for this tag are "yes", "no", and
|
||||
"clean".
|
||||
|
||||
If you populate this tag with "yes", an "explicit" parental advisory
|
||||
graphic will appear next to your podcast artwork on the iTunes Store and
|
||||
in the Name column in iTunes. If the value is "clean", the parental
|
||||
graphic will appear next to your podcast artwork on the iTunes Store
|
||||
and in the Name column in iTunes. If the value is "clean", the parental
|
||||
advisory type is considered Clean, meaning that no explicit language or
|
||||
adult content is included anywhere in the episodes, and a "clean" graphic
|
||||
will appear. If the explicit tag is present and has any other value
|
||||
(e.g., "no"), you see no indicator — blank is the default advisory type.
|
||||
adult content is included anywhere in the episodes, and a "clean"
|
||||
graphic will appear. If the explicit tag is present and has any other
|
||||
value (e.g., "no"), you see no indicator — blank is the default
|
||||
advisory type.
|
||||
|
||||
:param itunes_explicit: If the podcast episode contains explicit material.
|
||||
:param itunes_explicit: If the podcast episode contains explicit
|
||||
material.
|
||||
:returns: If the podcast episode contains explicit material.
|
||||
'''
|
||||
if not itunes_explicit is None:
|
||||
if not itunes_explicit in ('', 'yes', 'no', 'clean'):
|
||||
if itunes_explicit is not None:
|
||||
if itunes_explicit not in ('', 'yes', 'no', 'clean'):
|
||||
raise ValueError('Invalid value for explicit tag')
|
||||
self.__itunes_explicit = itunes_explicit
|
||||
return self.__itunes_explicit
|
||||
|
||||
|
||||
def itunes_is_closed_captioned(self, itunes_is_closed_captioned=None):
|
||||
'''Get or set the is_closed_captioned value of the podcast episode. This
|
||||
tag should be used if your podcast includes a video episode with embedded
|
||||
closed captioning support. The two values for this tag are "yes" and
|
||||
"no”.
|
||||
'''Get or set the is_closed_captioned value of the podcast episode.
|
||||
This tag should be used if your podcast includes a video episode with
|
||||
embedded closed captioning support. The two values for this tag are
|
||||
"yes" and "no”.
|
||||
|
||||
:param is_closed_captioned: If the episode has closed captioning support.
|
||||
:param is_closed_captioned: If the episode has closed captioning
|
||||
support.
|
||||
:returns: If the episode has closed captioning support.
|
||||
'''
|
||||
if not itunes_is_closed_captioned is None:
|
||||
self.__itunes_is_closed_captioned = itunes_is_closed_captioned in ('yes', True)
|
||||
if itunes_is_closed_captioned is not None:
|
||||
self.__itunes_is_closed_captioned = \
|
||||
itunes_is_closed_captioned in ('yes', True)
|
||||
return self.__itunes_is_closed_captioned
|
||||
|
||||
|
||||
def itunes_order(self, itunes_order=None):
|
||||
'''Get or set the itunes:order value of the podcast episode. This tag can
|
||||
be used to override the default ordering of episodes on the store.
|
||||
'''Get or set the itunes:order value of the podcast episode. This tag
|
||||
can be used to override the default ordering of episodes on the store.
|
||||
|
||||
This tag is used at an <item> level by populating with the number value
|
||||
in which you would like the episode to appear on the store. For example,
|
||||
if you would like an <item> to appear as the first episode in the
|
||||
podcast, you would populate the <itunes:order> tag with “1”. If
|
||||
conflicting order values are present in multiple episodes, the store will
|
||||
use default ordering (pubDate).
|
||||
in which you would like the episode to appear on the store. For
|
||||
example, if you would like an <item> to appear as the first episode in
|
||||
the podcast, you would populate the <itunes:order> tag with “1”. If
|
||||
conflicting order values are present in multiple episodes, the store
|
||||
will use default ordering (pubDate).
|
||||
|
||||
To remove the order from the episode set the order to a value below zero.
|
||||
To remove the order from the episode set the order to a value below
|
||||
zero.
|
||||
|
||||
:param itunes_order: The order of the episode.
|
||||
:returns: The order of the episode.
|
||||
'''
|
||||
if not itunes_order is None:
|
||||
if itunes_order is not None:
|
||||
self.__itunes_order = int(itunes_order)
|
||||
return self.__itunes_order
|
||||
|
||||
|
||||
def itunes_subtitle(self, itunes_subtitle=None):
|
||||
'''Get or set the itunes:subtitle value for the podcast episode. The
|
||||
contents of this tag are shown in the Description column in iTunes. The
|
||||
|
@ -222,22 +224,21 @@ class PodcastEntryExtension(BaseEntryExtension):
|
|||
:param itunes_subtitle: 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
|
||||
return self.__itunes_subtitle
|
||||
|
||||
|
||||
def itunes_summary(self, itunes_summary=None):
|
||||
'''Get or set the itunes:summary value for the podcast episode. The
|
||||
contents of this tag are shown in a separate window that appears when the
|
||||
"circled i" in the Description column is clicked. It also appears on the
|
||||
iTunes page for your podcast. This field can be up to 4000 characters. If
|
||||
<itunes:summary> is not included, the contents of the <description> tag
|
||||
are used.
|
||||
contents of this tag are shown in a separate window that appears when
|
||||
the "circled i" in the Description column is clicked. It also appears
|
||||
on the iTunes page for your podcast. This field can be up to 4000
|
||||
characters. If <itunes:summary> is not included, the contents of the
|
||||
<description> tag are used.
|
||||
|
||||
:param itunes_summary: 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
|
||||
return self.__itunes_summary
|
||||
|
|
|
@ -15,6 +15,7 @@ from feedgen.ext.base import BaseExtension,BaseEntryExtension
|
|||
|
||||
TORRENT_NS = 'http://xmlns.ezrss.it/0.1/dtd/'
|
||||
|
||||
|
||||
class TorrentExtension(BaseExtension):
|
||||
'''FeedGenerator extension for torrent feeds.
|
||||
'''
|
||||
|
@ -31,8 +32,6 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
self.__torrent_contentlength = None
|
||||
self.__torrent_seeds = None
|
||||
self.__torrent_peers = None
|
||||
self.__torrent=verified = None
|
||||
|
||||
|
||||
def extend_rss(self, entry):
|
||||
'''Add additional fields to an RSS item.
|
||||
|
@ -44,7 +43,8 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
filename.text = self.__torrent_filename
|
||||
|
||||
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
|
||||
|
||||
if self.__torrent_infohash:
|
||||
|
@ -65,14 +65,13 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
verified = etree.SubElement(entry, '{%s}verified' % TORRENT_NS)
|
||||
verified.text = self.__torrent_verified
|
||||
|
||||
|
||||
def filename(self, torrent_filename=None):
|
||||
'''Get or set the name of the torrent file.
|
||||
|
||||
:param torrent_filename: The name of the torrent file.
|
||||
:returns: The name of the torrent file.
|
||||
'''
|
||||
if not torrent_filename is None:
|
||||
if torrent_filename is not None:
|
||||
self.__torrent_filename = torrent_filename
|
||||
return self.__torrent_filename
|
||||
|
||||
|
@ -82,7 +81,7 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
:param torrent_infohash: The target file hash.
|
||||
:returns: The target hash file.
|
||||
'''
|
||||
if not torrent_infohash is None:
|
||||
if torrent_infohash is not None:
|
||||
self.__torrent_infohash = torrent_infohash
|
||||
return self.__torrent_infohash
|
||||
|
||||
|
@ -92,7 +91,7 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
:param torrent_contentlength: 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
|
||||
return self.__torrent_contentlength
|
||||
|
||||
|
@ -102,7 +101,7 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
:param torrent_seeds: The seeds number.
|
||||
:returns: The seeds number.
|
||||
'''
|
||||
if not torrent_seeds is None:
|
||||
if torrent_seeds is not None:
|
||||
self.__torrent_seeds = torrent_seeds
|
||||
return self.__torrent_seeds
|
||||
|
||||
|
@ -112,7 +111,7 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
:param torrent_infohash: The peers number.
|
||||
:returns: The peers number.
|
||||
'''
|
||||
if not torrent_peers is None:
|
||||
if torrent_peers is not None:
|
||||
self.__torrent_peers = torrent_peers
|
||||
return self.__torrent_peers
|
||||
|
||||
|
@ -122,6 +121,6 @@ class TorrentEntryExtension(BaseEntryExtension):
|
|||
:param torrent_infohash: 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
|
||||
return self.__torrent_verified
|
||||
|
|
346
feedgen/feed.py
346
feedgen/feed.py
|
@ -3,7 +3,7 @@
|
|||
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.
|
||||
|
||||
|
@ -27,11 +27,10 @@ class FeedGenerator(object):
|
|||
'''FeedGenerator for generating ATOM and RSS feeds.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.__feed_entries = []
|
||||
|
||||
## ATOM
|
||||
# ATOM
|
||||
# http://www.atomenabled.org/developers/syndication/
|
||||
# required
|
||||
self.__atom_id = None
|
||||
|
@ -57,7 +56,7 @@ class FeedGenerator(object):
|
|||
# other
|
||||
self.__atom_feed_xml_lang = None
|
||||
|
||||
## RSS
|
||||
# RSS
|
||||
# http://www.rssboard.org/rss-specification
|
||||
self.__rss_title = None
|
||||
self.__rss_link = None
|
||||
|
@ -83,9 +82,9 @@ class FeedGenerator(object):
|
|||
# Extension list:
|
||||
self.__extensions = {}
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
|
@ -95,15 +94,18 @@ class FeedGenerator(object):
|
|||
if ext.get('atom'):
|
||||
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:
|
||||
feed.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = \
|
||||
self.__atom_feed_xml_lang
|
||||
|
||||
if not (self.__atom_id and self.__atom_title and self.__atom_updated):
|
||||
missing = ', '.join(([] if self.__atom_title else ['title']) + \
|
||||
missing = ([] if self.__atom_title else ['title']) + \
|
||||
([] 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)
|
||||
id = etree.SubElement(feed, 'id')
|
||||
id.text = self.__atom_id
|
||||
|
@ -198,7 +200,6 @@ class FeedGenerator(object):
|
|||
doc = etree.ElementTree(feed)
|
||||
return feed, doc
|
||||
|
||||
|
||||
def atom_str(self, pretty=False, extensions=True, encoding='UTF-8',
|
||||
xml_declaration=True):
|
||||
'''Generates an ATOM feed and returns the feed XML as string.
|
||||
|
@ -213,20 +214,19 @@ class FeedGenerator(object):
|
|||
:returns: String representation of the ATOM feed.
|
||||
|
||||
**Return type:** The return type may vary between different Python
|
||||
versions and your encoding parameters passed to this method. For details
|
||||
have a look at the `lxml documentation
|
||||
versions and your encoding parameters passed to this method. For
|
||||
details have a look at the `lxml documentation
|
||||
<https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring>`_
|
||||
'''
|
||||
feed, doc = self._create_atom(extensions=extensions)
|
||||
return etree.tostring(feed, pretty_print=pretty, encoding=encoding,
|
||||
xml_declaration=xml_declaration)
|
||||
|
||||
|
||||
def atom_file(self, filename, extensions=True, pretty=False,
|
||||
encoding='UTF-8', xml_declaration=True):
|
||||
'''Generates an ATOM feed and write the resulting XML to a file.
|
||||
|
||||
:param filename: Name of file to write, or a file-like object, or a URL.
|
||||
:param filename: Name of file to write or a file-like object or a URL.
|
||||
:param extensions: Enable or disable the loaded extensions for the xml
|
||||
generation (default: enabled).
|
||||
:param pretty: If the feed should be split into multiple lines and
|
||||
|
@ -239,9 +239,9 @@ class FeedGenerator(object):
|
|||
doc.write(filename, pretty_print=pretty, encoding=encoding,
|
||||
xml_declaration=xml_declaration)
|
||||
|
||||
|
||||
def _create_rss(self, extensions=True):
|
||||
'''Create an RSS feed xml structure containing all previously set fields.
|
||||
'''Create an RSS feed xml structure containing all previously set
|
||||
fields.
|
||||
|
||||
:returns: Tuple containing the feed root element and the element tree.
|
||||
'''
|
||||
|
@ -256,10 +256,13 @@ class FeedGenerator(object):
|
|||
|
||||
feed = etree.Element('rss', version='2.0', nsmap=nsmap)
|
||||
channel = etree.SubElement(feed, 'channel')
|
||||
if not ( self.__rss_title and self.__rss_link and self.__rss_description ):
|
||||
missing = ', '.join(([] if self.__rss_title else ['title']) + \
|
||||
if not (self.__rss_title and
|
||||
self.__rss_link and
|
||||
self.__rss_description):
|
||||
missing = ([] if self.__rss_title else ['title']) + \
|
||||
([] if self.__rss_link else ['link']) + \
|
||||
([] if self.__rss_description else ['description']))
|
||||
([] if self.__rss_description else ['description'])
|
||||
missing = ', '.join(missing)
|
||||
raise ValueError('Required fields not set (%s)' % missing)
|
||||
title = etree.SubElement(channel, 'title')
|
||||
title.text = self.__rss_title
|
||||
|
@ -270,8 +273,8 @@ class FeedGenerator(object):
|
|||
for ln in self.__atom_link or []:
|
||||
# It is recommended to include a atom self link in rss documents…
|
||||
if ln.get('rel') == 'self':
|
||||
selflink = etree.SubElement(channel,
|
||||
'{http://www.w3.org/2005/Atom}link',
|
||||
selflink = etree.SubElement(
|
||||
channel, '{http://www.w3.org/2005/Atom}link',
|
||||
href=ln['href'], rel='self')
|
||||
if ln.get('type'):
|
||||
selflink.attrib['type'] = ln['type']
|
||||
|
@ -310,11 +313,9 @@ class FeedGenerator(object):
|
|||
url = etree.SubElement(image, 'url')
|
||||
url.text = self.__rss_image.get('url')
|
||||
title = etree.SubElement(image, 'title')
|
||||
title.text = self.__rss_image['title'] \
|
||||
if self.__rss_image.get('title') else self.__rss_title
|
||||
title.text = self.__rss_image.get('title', self.__rss_title)
|
||||
link = etree.SubElement(image, 'link')
|
||||
link.text = self.__rss_image['link'] \
|
||||
if self.__rss_image.get('link') else self.__rss_link
|
||||
link.text = self.__rss_image.get('link', self.__rss_link)
|
||||
if self.__rss_image.get('width'):
|
||||
width = etree.SubElement(image, 'width')
|
||||
width.text = self.__rss_image.get('width')
|
||||
|
@ -353,7 +354,8 @@ class FeedGenerator(object):
|
|||
if self.__rss_textInput:
|
||||
textInput = etree.SubElement(channel, 'textInput')
|
||||
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['link'] = self.__rss_textInput.get('link')
|
||||
if self.__rss_ttl:
|
||||
|
@ -375,7 +377,6 @@ class FeedGenerator(object):
|
|||
doc = etree.ElementTree(feed)
|
||||
return feed, doc
|
||||
|
||||
|
||||
def rss_str(self, pretty=False, extensions=True, encoding='UTF-8',
|
||||
xml_declaration=True):
|
||||
'''Generates an RSS feed and returns the feed XML as string.
|
||||
|
@ -390,20 +391,19 @@ class FeedGenerator(object):
|
|||
:returns: String representation of the RSS feed.
|
||||
|
||||
**Return type:** The return type may vary between different Python
|
||||
versions and your encoding parameters passed to this method. For details
|
||||
have a look at the `lxml documentation
|
||||
versions and your encoding parameters passed to this method. For
|
||||
details have a look at the `lxml documentation
|
||||
<https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring>`_
|
||||
'''
|
||||
feed, doc = self._create_rss(extensions=extensions)
|
||||
return etree.tostring(feed, pretty_print=pretty, encoding=encoding,
|
||||
xml_declaration=xml_declaration)
|
||||
|
||||
|
||||
def rss_file(self, filename, extensions=True, pretty=False,
|
||||
encoding='UTF-8', xml_declaration=True):
|
||||
'''Generates an RSS feed and write the resulting XML to a file.
|
||||
|
||||
:param filename: Name of file to write, or a file-like object, or a URL.
|
||||
:param filename: Name of file to write or a file-like object or a URL.
|
||||
:param extensions: Enable or disable the loaded extensions for the xml
|
||||
generation (default: enabled).
|
||||
:param pretty: If the feed should be split into multiple lines and
|
||||
|
@ -416,7 +416,6 @@ class FeedGenerator(object):
|
|||
doc.write(filename, pretty_print=pretty, encoding=encoding,
|
||||
xml_declaration=xml_declaration)
|
||||
|
||||
|
||||
def title(self, title=None):
|
||||
'''Get or set the title value of the feed. It should contain a human
|
||||
readable title for the feed. Often the same as the title of the
|
||||
|
@ -426,12 +425,11 @@ class FeedGenerator(object):
|
|||
:param title: The new title of the feed.
|
||||
:returns: The feeds title.
|
||||
'''
|
||||
if not title is None:
|
||||
if title is not None:
|
||||
self.__atom_title = title
|
||||
self.__rss_title = title
|
||||
return self.__atom_title
|
||||
|
||||
|
||||
def id(self, id=None):
|
||||
'''Get or set the feed id which identifies the feed using a universally
|
||||
unique and permanent URI. If you have a long-term, renewable lease on
|
||||
|
@ -442,17 +440,16 @@ class FeedGenerator(object):
|
|||
:returns: Id of the feed.
|
||||
'''
|
||||
|
||||
if not id is None:
|
||||
if id is not None:
|
||||
self.__atom_id = id
|
||||
return self.__atom_id
|
||||
|
||||
|
||||
def updated(self, updated=None):
|
||||
'''Set or get the updated value which indicates the last time the feed
|
||||
was modified in a significant way.
|
||||
|
||||
The value can either be a string which will automatically be parsed or a
|
||||
datetime.datetime object. In any case it is necessary that the value
|
||||
The value can either be a string which will automatically be parsed or
|
||||
a datetime.datetime object. In any case it is necessary that the value
|
||||
include timezone information.
|
||||
|
||||
This will set both atom:updated and rss:lastBuildDate.
|
||||
|
@ -463,7 +460,7 @@ class FeedGenerator(object):
|
|||
:param updated: The modification date.
|
||||
:returns: Modification date as datetime.datetime
|
||||
'''
|
||||
if not updated is None:
|
||||
if updated is not None:
|
||||
if isinstance(updated, string_types):
|
||||
updated = dateutil.parser.parse(updated)
|
||||
if not isinstance(updated, datetime):
|
||||
|
@ -475,13 +472,12 @@ class FeedGenerator(object):
|
|||
|
||||
return self.__atom_updated
|
||||
|
||||
|
||||
def lastBuildDate(self, lastBuildDate=None):
|
||||
'''Set or get the lastBuildDate value which indicates the last time the
|
||||
content of the channel changed.
|
||||
|
||||
The value can either be a string which will automatically be parsed or a
|
||||
datetime.datetime object. In any case it is necessary that the value
|
||||
The value can either be a string which will automatically be parsed or
|
||||
a datetime.datetime object. In any case it is necessary that the value
|
||||
include timezone information.
|
||||
|
||||
This will set both atom:updated and rss:lastBuildDate.
|
||||
|
@ -494,11 +490,10 @@ class FeedGenerator(object):
|
|||
'''
|
||||
return self.updated(lastBuildDate)
|
||||
|
||||
|
||||
def author(self, author=None, replace=False, **kwargs):
|
||||
'''Get or set author data. An author element is a dictionary containing a name,
|
||||
an email address and a URI. Name is mandatory for ATOM, email is mandatory
|
||||
for RSS.
|
||||
'''Get or set author data. An author element is a dictionary containing
|
||||
a name, an email address and a URI. Name is mandatory for ATOM, email
|
||||
is mandatory for RSS.
|
||||
|
||||
This method can be called with:
|
||||
|
||||
|
@ -525,27 +520,29 @@ class FeedGenerator(object):
|
|||
[{'name':'John Doe','email':'jdoe@example.com'},
|
||||
{'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'}]
|
||||
|
||||
'''
|
||||
if author is None and kwargs:
|
||||
author = kwargs
|
||||
if not author is None:
|
||||
if author is not None:
|
||||
if replace or self.__atom_author is None:
|
||||
self.__atom_author = []
|
||||
self.__atom_author += ensure_format(author,
|
||||
set(['name', 'email', 'uri']), set(['name']))
|
||||
set(['name', 'email', 'uri']),
|
||||
set(['name']))
|
||||
self.__rss_author = []
|
||||
for a in self.__atom_author:
|
||||
if a.get('email'):
|
||||
self.__rss_author.append(a['email'])
|
||||
return self.__atom_author
|
||||
|
||||
|
||||
def link(self, link=None, replace=False, **kwargs):
|
||||
'''Get or set link data. An link element is a dict with the fields href,
|
||||
rel, type, hreflang, title, and length. Href is mandatory for ATOM.
|
||||
'''Get or set link data. An link element is a dict with the fields
|
||||
href, rel, type, hreflang, title, and length. Href is mandatory for
|
||||
ATOM.
|
||||
|
||||
This method can be called with:
|
||||
|
||||
|
@ -560,8 +557,8 @@ class FeedGenerator(object):
|
|||
or one of the following predefined values (default=alternate):
|
||||
|
||||
- *alternate* an alternate representation of the entry or feed, for
|
||||
example a permalink to the html version of the entry, or the front
|
||||
page of the weblog.
|
||||
example a permalink to the html version of the entry, or the
|
||||
front page of the weblog.
|
||||
- *enclosure* a related resource which is potentially large in size
|
||||
and might require special handling, for example an audio or video
|
||||
recording.
|
||||
|
@ -589,24 +586,30 @@ class FeedGenerator(object):
|
|||
'''
|
||||
if link is None and kwargs:
|
||||
link = kwargs
|
||||
if not link is None:
|
||||
if link is not None:
|
||||
if replace or self.__atom_link is None:
|
||||
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': [
|
||||
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark',
|
||||
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form',
|
||||
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure',
|
||||
'duplicate', 'edit', 'edit-form', 'edit-media', 'enclosure', 'first', 'glossary',
|
||||
'help', 'hosts', 'hub', 'icon', 'index', 'item', 'last', 'latest-version', 'license',
|
||||
'lrdd', 'memento', 'monitor', 'monitor-group', 'next', 'next-archive', 'nofollow',
|
||||
'noreferrer', 'original', 'payment', 'predecessor-version', 'prefetch', 'prev', 'preview',
|
||||
'previous', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies', 'search',
|
||||
'section', 'self', 'service', 'start', 'stylesheet', 'subsection', 'successor-version',
|
||||
'tag', 'terms-of-service', 'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
|
||||
'working-copy', 'working-copy-of'
|
||||
'about', 'alternate', 'appendix', 'archives', 'author',
|
||||
'bookmark', 'canonical', 'chapter', 'collection',
|
||||
'contents', 'copyright', 'create-form', 'current',
|
||||
'derivedfrom', 'describedby', 'describes', 'disclosure',
|
||||
'duplicate', 'edit', 'edit-form', 'edit-media',
|
||||
'enclosure', 'first', 'glossary', 'help', 'hosts', 'hub',
|
||||
'icon', 'index', 'item', 'last', 'latest-version',
|
||||
'license', 'lrdd', 'memento', 'monitor', 'monitor-group',
|
||||
'next', 'next-archive', 'nofollow', 'noreferrer',
|
||||
'original', 'payment', 'predecessor-version', 'prefetch',
|
||||
'prev', 'preview', 'previous', 'prev-archive',
|
||||
'privacy-policy', 'profile', 'related', 'replies',
|
||||
'search', 'section', 'self', 'service', 'start',
|
||||
'stylesheet', 'subsection', 'successor-version', 'tag',
|
||||
'terms-of-service', 'timegate', 'timemap', 'type', 'up',
|
||||
'version-history', 'via', 'working-copy', 'working-copy-of'
|
||||
]})
|
||||
# RSS only needs one URL. We use the first link for RSS:
|
||||
if len(self.__atom_link) > 0:
|
||||
|
@ -614,7 +617,6 @@ class FeedGenerator(object):
|
|||
# return the set with more information (atom)
|
||||
return self.__atom_link
|
||||
|
||||
|
||||
def category(self, category=None, replace=False, **kwargs):
|
||||
'''Get or set categories that the feed belongs to.
|
||||
|
||||
|
@ -630,8 +632,8 @@ class FeedGenerator(object):
|
|||
- *scheme* identifies the categorization scheme via a URI.
|
||||
- *label* provides a human-readable label for display
|
||||
|
||||
If a label is present it is used for the RSS feeds. Otherwise the term is
|
||||
used. The scheme is used for the domain attribute in RSS.
|
||||
If a label is present it is used for the RSS feeds. Otherwise the term
|
||||
is used. The scheme is used for the domain attribute in RSS.
|
||||
|
||||
:param link: Dict or list of dicts with data.
|
||||
:param replace: Add or replace old data.
|
||||
|
@ -639,7 +641,7 @@ class FeedGenerator(object):
|
|||
'''
|
||||
if category is None and kwargs:
|
||||
category = kwargs
|
||||
if not category is None:
|
||||
if category is not None:
|
||||
if replace or self.__atom_category is None:
|
||||
self.__atom_category = []
|
||||
self.__atom_category += ensure_format(
|
||||
|
@ -652,18 +654,17 @@ class FeedGenerator(object):
|
|||
self.__rss_category = []
|
||||
for cat in self.__atom_category:
|
||||
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'):
|
||||
rss_cat['domain'] = cat['scheme']
|
||||
self.__rss_category.append(rss_cat)
|
||||
return self.__atom_category
|
||||
|
||||
|
||||
def cloud(self, domain=None, port=None, path=None, registerProcedure=None,
|
||||
protocol=None):
|
||||
'''Set or get the cloud data of the feed. It is an RSS only attribute. It
|
||||
specifies a web service that supports the rssCloud interface which can be
|
||||
implemented in HTTP-POST, XML-RPC or SOAP 1.1.
|
||||
'''Set or get the cloud data of the feed. It is an RSS only attribute.
|
||||
It specifies a web service that supports the rssCloud interface which
|
||||
can be implemented in HTTP-POST, XML-RPC or SOAP 1.1.
|
||||
|
||||
:param domain: The domain where the webservice can be found.
|
||||
:param port: The port the webservice listens to.
|
||||
|
@ -672,12 +673,12 @@ class FeedGenerator(object):
|
|||
:param protocol: Can be either HTTP-POST, XML-RPC or SOAP 1.1.
|
||||
: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,
|
||||
'registerProcedure':registerProcedure, 'protocol':protocol}
|
||||
'registerProcedure': registerProcedure,
|
||||
'protocol': protocol}
|
||||
return self.__rss_cloud
|
||||
|
||||
|
||||
def contributor(self, contributor=None, replace=False, **kwargs):
|
||||
'''Get or set the contributor data of the feed. This is an ATOM only
|
||||
value.
|
||||
|
@ -692,74 +693,73 @@ class FeedGenerator(object):
|
|||
- *uri* contains a home page for the person.
|
||||
- *email* contains an email address for the person.
|
||||
|
||||
:param contributor: Dictionary or list of dictionaries with contributor data.
|
||||
:param contributor: Dictionary or list of dictionaries with contributor
|
||||
data.
|
||||
:param replace: Add or replace old data.
|
||||
:returns: List of contributors as dictionaries.
|
||||
'''
|
||||
if contributor is None and kwargs:
|
||||
contributor = kwargs
|
||||
if not contributor is None:
|
||||
if contributor is not None:
|
||||
if replace or self.__atom_contributor is None:
|
||||
self.__atom_contributor = []
|
||||
self.__atom_contributor += ensure_format( contributor,
|
||||
set(['name', 'email', 'uri']), set(['name']))
|
||||
self.__atom_contributor += ensure_format(
|
||||
contributor, set(['name', 'email', 'uri']), set(['name']))
|
||||
return self.__atom_contributor
|
||||
|
||||
|
||||
def generator(self, generator=None, version=None, uri=None):
|
||||
'''Get or the generator of the feed which identifies the software used to
|
||||
generate the feed, for debugging and other purposes. Both the uri and
|
||||
version attributes are optional and only available in the ATOM feed.
|
||||
'''Get or the generator of the feed which identifies the software used
|
||||
to generate the feed, for debugging and other purposes. Both the uri
|
||||
and version attributes are optional and only available in the ATOM
|
||||
feed.
|
||||
|
||||
:param generator: Software used to create the feed.
|
||||
:param version: Version of the software.
|
||||
:param uri: URI the software can be found.
|
||||
'''
|
||||
if not generator is None:
|
||||
if generator is not None:
|
||||
self.__atom_generator = {'value': generator}
|
||||
if not version is None:
|
||||
if version is not None:
|
||||
self.__atom_generator['version'] = version
|
||||
if not uri is None:
|
||||
if uri is not None:
|
||||
self.__atom_generator['uri'] = uri
|
||||
self.__rss_generator = generator
|
||||
return self.__atom_generator
|
||||
|
||||
|
||||
def icon(self, icon=None):
|
||||
'''Get or set the icon of the feed which is a small image which provides
|
||||
iconic visual identification for the feed. Icons should be square. This
|
||||
is an ATOM only value.
|
||||
'''Get or set the icon of the feed which is a small image which
|
||||
provides iconic visual identification for the feed. Icons should be
|
||||
square. This is an ATOM only value.
|
||||
|
||||
:param icon: URI of the feeds icon.
|
||||
:returns: URI of the feeds icon.
|
||||
'''
|
||||
if not icon is None:
|
||||
if icon is not None:
|
||||
self.__atom_icon = icon
|
||||
return self.__atom_icon
|
||||
|
||||
|
||||
def logo(self, logo=None):
|
||||
'''Get or set the logo of the feed which is a larger image which provides
|
||||
visual identification for the feed. Images should be twice as wide as
|
||||
they are tall. This is an ATOM value but will also set the rss:image
|
||||
value.
|
||||
'''Get or set the logo of the feed which is a larger image which
|
||||
provides visual identification for the feed. Images should be twice as
|
||||
wide as they are tall. This is an ATOM value but will also set the
|
||||
rss:image value.
|
||||
|
||||
:param logo: Logo of the feed.
|
||||
:returns: Logo of the feed.
|
||||
'''
|
||||
if not logo is None:
|
||||
if logo is not None:
|
||||
self.__atom_logo = logo
|
||||
self.__rss_image = {'url': logo}
|
||||
return self.__atom_logo
|
||||
|
||||
|
||||
def image(self, url=None, title=None, link=None, width=None, height=None,
|
||||
description=None):
|
||||
'''Set the image of the feed. This element is roughly equivalent to
|
||||
atom:logo.
|
||||
|
||||
:param url: The URL of a GIF, JPEG or PNG image.
|
||||
:param title: Describes the image. The default value is the feeds title.
|
||||
:param title: Describes the image. The default value is the feeds
|
||||
title.
|
||||
:param link: URL of the site the image will link to. The default is to
|
||||
use the feeds first altertate link.
|
||||
:param width: Width of the image in pixel. The maximum is 144.
|
||||
|
@ -767,11 +767,11 @@ class FeedGenerator(object):
|
|||
:param description: Title of the link.
|
||||
:returns: Data of the image as dictionary.
|
||||
'''
|
||||
if not url is None:
|
||||
if url is not None:
|
||||
self.__rss_image = {'url': url}
|
||||
if not title is None:
|
||||
if title is not None:
|
||||
self.__rss_image['title'] = title
|
||||
if not link is None:
|
||||
if link is not None:
|
||||
self.__rss_image['link'] = link
|
||||
if width:
|
||||
self.__rss_image['width'] = width
|
||||
|
@ -780,20 +780,18 @@ class FeedGenerator(object):
|
|||
self.__atom_logo = url
|
||||
return self.__rss_image
|
||||
|
||||
|
||||
def rights(self, rights=None):
|
||||
'''Get or set the rights value of the feed which conveys information
|
||||
about rights, e.g. copyrights, held in and over the feed. This ATOM value
|
||||
will also set rss:copyright.
|
||||
about rights, e.g. copyrights, held in and over the feed. This ATOM
|
||||
value will also set rss:copyright.
|
||||
|
||||
:param rights: Rights information of the feed.
|
||||
'''
|
||||
if not rights is None:
|
||||
if rights is not None:
|
||||
self.__atom_rights = rights
|
||||
self.__rss_copyright = rights
|
||||
return self.__atom_rights
|
||||
|
||||
|
||||
def copyright(self, copyright=None):
|
||||
'''Get or set the copyright notice for content in the channel. This RSS
|
||||
value will also set the atom:rights value.
|
||||
|
@ -803,7 +801,6 @@ class FeedGenerator(object):
|
|||
'''
|
||||
return self.rights(copyright)
|
||||
|
||||
|
||||
def subtitle(self, subtitle=None):
|
||||
'''Get or set the subtitle value of the cannel which contains a
|
||||
human-readable description or subtitle for the feed. This ATOM property
|
||||
|
@ -812,17 +809,16 @@ class FeedGenerator(object):
|
|||
:param subtitle: 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.__rss_description = subtitle
|
||||
return self.__atom_subtitle
|
||||
|
||||
|
||||
def description(self, description=None):
|
||||
'''Set and get the description of the feed. This is an RSS only element
|
||||
which is a phrase or sentence describing the channel. It is mandatory for
|
||||
RSS feeds. It is roughly the same as atom:subtitle. Thus setting this
|
||||
will also set atom:subtitle.
|
||||
which is a phrase or sentence describing the channel. It is mandatory
|
||||
for RSS feeds. It is roughly the same as atom:subtitle. Thus setting
|
||||
this will also set atom:subtitle.
|
||||
|
||||
:param description: Description of the channel.
|
||||
:returns: Description of the channel.
|
||||
|
@ -830,61 +826,58 @@ class FeedGenerator(object):
|
|||
'''
|
||||
return self.subtitle(description)
|
||||
|
||||
|
||||
def docs(self, docs=None):
|
||||
'''Get or set the docs value of the feed. This is an RSS only value. It
|
||||
is a URL that points to the documentation for the format used in the RSS
|
||||
file. It is probably a pointer to [1]. It is for people who might stumble
|
||||
across an RSS file on a Web server 25 years from now and wonder what it
|
||||
is.
|
||||
is a URL that points to the documentation for the format used in the
|
||||
RSS file. It is probably a pointer to [1]. It is for people who might
|
||||
stumble across an RSS file on a Web server 25 years from now and wonder
|
||||
what it is.
|
||||
|
||||
[1]: http://www.rssboard.org/rss-specification
|
||||
|
||||
:param docs: URL of the format documentation.
|
||||
:returns: URL of the format documentation.
|
||||
'''
|
||||
if not docs is None:
|
||||
if docs is not None:
|
||||
self.__rss_docs = docs
|
||||
return self.__rss_docs
|
||||
|
||||
|
||||
def language(self, language=None):
|
||||
'''Get or set the language of the feed. It indicates the language the
|
||||
channel is written in. This allows aggregators to group all Italian
|
||||
language sites, for example, on a single page. This is an RSS only field.
|
||||
However, this value will also be used to set the xml:lang property of the
|
||||
ATOM feed node.
|
||||
language sites, for example, on a single page. This is an RSS only
|
||||
field. However, this value will also be used to set the xml:lang
|
||||
property of the ATOM feed node.
|
||||
The value should be an IETF language tag.
|
||||
|
||||
:param language: Language of the feed.
|
||||
:returns: Language of the feed.
|
||||
'''
|
||||
if not language is None:
|
||||
if language is not None:
|
||||
self.__rss_language = language
|
||||
self.__atom_feed_xml_lang = language
|
||||
return self.__rss_language
|
||||
|
||||
|
||||
def managingEditor(self, managingEditor=None):
|
||||
'''Set or get the value for managingEditor which is the email address for
|
||||
person responsible for editorial content. This is a RSS only value.
|
||||
'''Set or get the value for managingEditor which is the email address
|
||||
for person responsible for editorial content. This is a RSS only
|
||||
value.
|
||||
|
||||
:param managingEditor: Email 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
|
||||
return self.__rss_managingEditor
|
||||
|
||||
|
||||
def pubDate(self, pubDate=None):
|
||||
'''Set or get the publication date for the content in the channel. For
|
||||
example, the New York Times publishes on a daily basis, the publication
|
||||
date flips once every 24 hours. That's when the pubDate of the channel
|
||||
changes.
|
||||
|
||||
The value can either be a string which will automatically be parsed or a
|
||||
datetime.datetime object. In any case it is necessary that the value
|
||||
The value can either be a string which will automatically be parsed or
|
||||
a datetime.datetime object. In any case it is necessary that the value
|
||||
include timezone information.
|
||||
|
||||
This will set both atom:updated and rss:lastBuildDate.
|
||||
|
@ -892,7 +885,7 @@ class FeedGenerator(object):
|
|||
:param pubDate: The publication date.
|
||||
:returns: Publication date as datetime.datetime
|
||||
'''
|
||||
if not pubDate is None:
|
||||
if pubDate is not None:
|
||||
if isinstance(pubDate, string_types):
|
||||
pubDate = dateutil.parser.parse(pubDate)
|
||||
if not isinstance(pubDate, datetime):
|
||||
|
@ -903,76 +896,73 @@ class FeedGenerator(object):
|
|||
|
||||
return self.__rss_pubDate
|
||||
|
||||
|
||||
def rating(self, rating=None):
|
||||
'''Set and get the PICS rating for the channel. It is an RSS only
|
||||
value.
|
||||
'''
|
||||
if not rating is None:
|
||||
if rating is not None:
|
||||
self.__rss_rating = rating
|
||||
return self.__rss_rating
|
||||
|
||||
|
||||
def skipHours(self, hours=None, replace=False):
|
||||
'''Set or get the value of skipHours, a hint for aggregators telling them
|
||||
which hours they can skip. This is an RSS only value.
|
||||
'''Set or get the value of skipHours, a hint for aggregators telling
|
||||
them which hours they can skip. This is an RSS only value.
|
||||
|
||||
This method can be called with an hour or a list of hours. The hours are
|
||||
represented as integer values from 0 to 23.
|
||||
This method can be called with an hour or a list of hours. The hours
|
||||
are represented as integer values from 0 to 23.
|
||||
|
||||
:param hours: List of hours the feedreaders should not check the feed.
|
||||
:param replace: Add or replace old data.
|
||||
:returns: List of hours the feedreaders should not check the feed.
|
||||
'''
|
||||
if not hours is None:
|
||||
if hours is not None:
|
||||
if not (isinstance(hours, list) or isinstance(hours, set)):
|
||||
hours = [hours]
|
||||
for h in hours:
|
||||
if not h in range(24):
|
||||
if h not in range(24):
|
||||
raise ValueError('Invalid hour %s' % h)
|
||||
if replace or not self.__rss_skipHours:
|
||||
self.__rss_skipHours = set()
|
||||
self.__rss_skipHours |= set(hours)
|
||||
return self.__rss_skipHours
|
||||
|
||||
|
||||
def skipDays(self, days=None, replace=False):
|
||||
'''Set or get the value of skipDays, a hint for aggregators telling them
|
||||
which days they can skip This is an RSS only value.
|
||||
'''Set or get the value of skipDays, a hint for aggregators telling
|
||||
them which days they can skip This is an RSS only value.
|
||||
|
||||
This method can be called with a day name or a list of day names. The days are
|
||||
represented as strings from 'Monday' to 'Sunday'.
|
||||
This method can be called with a day name or a list of day names. The
|
||||
days are represented as strings from 'Monday' to 'Sunday'.
|
||||
|
||||
:param hours: List of days the feedreaders should not check the feed.
|
||||
:param replace: Add or replace old data.
|
||||
:returns: List of days the feedreaders should not check the feed.
|
||||
'''
|
||||
if not days is None:
|
||||
if days is not None:
|
||||
if not (isinstance(days, list) or isinstance(days, set)):
|
||||
days = [days]
|
||||
for d in days:
|
||||
if not d in ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
|
||||
if d not in ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
|
||||
'Friday', 'Saturday', 'Sunday']:
|
||||
raise ValueError('Invalid day %s' % h)
|
||||
raise ValueError('Invalid day %s' % d)
|
||||
if replace or not self.__rss_skipDays:
|
||||
self.__rss_skipDays = set()
|
||||
self.__rss_skipDays |= set(days)
|
||||
return self.__rss_skipDays
|
||||
|
||||
|
||||
def textInput(self, title=None, description=None, name=None, link=None):
|
||||
'''Get or set the value of textInput. This is an RSS only field. The
|
||||
purpose of the <textInput> element is something of a mystery. You can use
|
||||
it to specify a search engine box. Or to allow a reader to provide
|
||||
purpose of the <textInput> element is something of a mystery. You can
|
||||
use it to specify a search engine box. Or to allow a reader to provide
|
||||
feedback. Most aggregators ignore it.
|
||||
|
||||
:param title: The label of the Submit button in the text input area.
|
||||
:param description: Explains the text input area.
|
||||
:param name: The name of the text object in the text input area.
|
||||
:param link: The URL of the CGI script that processes text input requests.
|
||||
:param link: The URL of the CGI script that processes text input
|
||||
requests.
|
||||
:returns: Dictionary containing textInput values.
|
||||
'''
|
||||
if not title is None:
|
||||
if title is not None:
|
||||
self.__rss_textInput = {}
|
||||
self.__rss_textInput['title'] = title
|
||||
self.__rss_textInput['description'] = description
|
||||
|
@ -980,37 +970,35 @@ class FeedGenerator(object):
|
|||
self.__rss_textInput['link'] = link
|
||||
return self.__rss_textInput
|
||||
|
||||
|
||||
def ttl(self, ttl=None):
|
||||
'''Get or set the ttl value. It is an RSS only element. ttl stands for
|
||||
time to live. It's a number of minutes that indicates how long a channel
|
||||
can be cached before refreshing from the source.
|
||||
time to live. It's a number of minutes that indicates how long a
|
||||
channel can be cached before refreshing from the source.
|
||||
|
||||
:param ttl: Integer value indicating how long the channel may be cached.
|
||||
:param ttl: Integer value indicating how long the channel may be
|
||||
cached.
|
||||
:returns: Time to live.
|
||||
'''
|
||||
if not ttl is None:
|
||||
if ttl is not None:
|
||||
self.__rss_ttl = int(ttl)
|
||||
return self.__rss_ttl
|
||||
|
||||
|
||||
def webMaster(self, webMaster=None):
|
||||
'''Get and set the value of webMaster, which represents the email address
|
||||
for the person responsible for technical issues relating to the feed.
|
||||
This is an RSS only value.
|
||||
'''Get and set the value of webMaster, which represents the email
|
||||
address for the person responsible for technical issues relating to the
|
||||
feed. This is an RSS only value.
|
||||
|
||||
:param webMaster: Email address of the webmaster.
|
||||
:returns: Email address of the webmaster.
|
||||
'''
|
||||
if not webMaster is None:
|
||||
if webMaster is not None:
|
||||
self.__rss_webMaster = webMaster
|
||||
return self.__rss_webMaster
|
||||
|
||||
|
||||
def add_entry(self, feedEntry=None):
|
||||
'''This method will add a new entry to the feed. If the feedEntry
|
||||
argument is omittet a new Entry object is created automatically. This is
|
||||
the prefered way to add new entries to a feed.
|
||||
argument is omittet a new Entry object is created automatically. This
|
||||
is the prefered way to add new entries to a feed.
|
||||
|
||||
:param feedEntry: FeedEntry object to add.
|
||||
:returns: FeedEntry object created or passed to this function.
|
||||
|
@ -1037,14 +1025,14 @@ class FeedGenerator(object):
|
|||
try:
|
||||
feedEntry.register_extension(extname,
|
||||
ext['extension_class_entry'],
|
||||
ext['atom'], ext['rss'] )
|
||||
ext['atom'],
|
||||
ext['rss'])
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
self.__feed_entries.append(feedEntry)
|
||||
return feedEntry
|
||||
|
||||
|
||||
def add_item(self, item=None):
|
||||
'''This method will add a new item to the feed. If the item argument is
|
||||
omittet a new FeedEntry object is created automatically. This is just
|
||||
|
@ -1052,7 +1040,6 @@ class FeedGenerator(object):
|
|||
'''
|
||||
return self.add_entry(item)
|
||||
|
||||
|
||||
def entry(self, entry=None, replace=False):
|
||||
'''Get or set feed entries. Use the add_entry() method instead to
|
||||
automatically create the FeedEntry objects.
|
||||
|
@ -1062,7 +1049,7 @@ class FeedGenerator(object):
|
|||
:param entry: FeedEntry object or list of FeedEntry objects.
|
||||
:returns: List ob all feed entries.
|
||||
'''
|
||||
if not entry is None:
|
||||
if entry is not None:
|
||||
if not isinstance(entry, list):
|
||||
entry = [entry]
|
||||
if replace:
|
||||
|
@ -1088,13 +1075,11 @@ class FeedGenerator(object):
|
|||
self.__feed_entries += entry
|
||||
return self.__feed_entries
|
||||
|
||||
|
||||
def item(self, item=None, replace=False):
|
||||
'''Get or set feed items. This is just another name for entry(...)
|
||||
'''
|
||||
return self.entry(item, replace)
|
||||
|
||||
|
||||
def remove_entry(self, entry):
|
||||
'''Remove a single entry from the feed. This method accepts both the
|
||||
FeedEntry object to remove or the index of the entry as argument.
|
||||
|
@ -1106,14 +1091,12 @@ class FeedGenerator(object):
|
|||
else:
|
||||
self.__feed_entries.pop(entry)
|
||||
|
||||
|
||||
def remove_item(self, item):
|
||||
'''Remove a single item from the feed. This is another name for
|
||||
remove_entry.
|
||||
'''
|
||||
self.remove_entry(item)
|
||||
|
||||
|
||||
def load_extension(self, name, atom=True, rss=True):
|
||||
'''Load a specific extension by name.
|
||||
|
||||
|
@ -1145,7 +1128,6 @@ class FeedGenerator(object):
|
|||
entryext = None
|
||||
self.register_extension(name, feedext, entryext, atom, rss)
|
||||
|
||||
|
||||
def register_extension(self, namespace, extension_class_feed=None,
|
||||
extension_class_entry=None, atom=True, rss=True):
|
||||
'''Registers an extension by class.
|
||||
|
@ -1180,6 +1162,8 @@ class FeedGenerator(object):
|
|||
for entry in self.__feed_entries:
|
||||
try:
|
||||
entry.register_extension(namespace,
|
||||
extension_class_entry, atom, rss)
|
||||
extension_class_entry,
|
||||
atom,
|
||||
rss)
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
:copyright: 2013, Lars Kiesow <lkiesow@uos.de>
|
||||
: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):
|
||||
|
@ -19,7 +20,8 @@ def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
|
|||
:param val: Dictionaries to check.
|
||||
:param allowed: Set of allowed keys.
|
||||
:param required: Set of required keys.
|
||||
:param allowed_values: Dictionary with keys and sets of their allowed values.
|
||||
:param allowed_values: Dictionary with keys and sets of their allowed
|
||||
values.
|
||||
:param defaults: Dictionary with default values.
|
||||
:returns: List of checked dictionaries.
|
||||
'''
|
||||
|
|
9
setup.py
9
setup.py
|
@ -6,8 +6,7 @@ import feedgen.version
|
|||
|
||||
packages = ['feedgen', 'feedgen/ext']
|
||||
|
||||
setup(
|
||||
name = 'feedgen',
|
||||
setup(name='feedgen',
|
||||
packages=packages,
|
||||
version=feedgen.version.version_full_str,
|
||||
description='Feed Generator (ATOM, RSS, Podcasts)',
|
||||
|
@ -23,7 +22,8 @@ setup(
|
|||
'Intended Audience :: Information Technology',
|
||||
'Intended Audience :: Science/Research',
|
||||
'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',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
|
@ -46,5 +46,4 @@ Podcasts.
|
|||
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
|
||||
at license.bsd and license.lgpl.
|
||||
'''
|
||||
)
|
||||
''')
|
||||
|
|
|
@ -7,9 +7,9 @@ These are test cases for a basic entry.
|
|||
"""
|
||||
|
||||
import unittest
|
||||
from lxml import etree
|
||||
from feedgen.feed import FeedGenerator
|
||||
|
||||
|
||||
class TestSequenceFunctions(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -49,7 +49,7 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
def test_checkEntryContent(self):
|
||||
|
||||
fg = self.fg
|
||||
assert len(fg.entry()) != None
|
||||
assert fg.entry()
|
||||
|
||||
def test_removeEntryByIndex(self):
|
||||
fg = FeedGenerator()
|
||||
|
@ -103,4 +103,3 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
fe.content('content', type='CDATA')
|
||||
result = fg.atom_str()
|
||||
assert b'<content type="CDATA"><![CDATA[content]]></content>' in result
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ from lxml import etree
|
|||
|
||||
class TestExtensionSyndication(unittest.TestCase):
|
||||
|
||||
SYN_NS = {'sy': 'http://purl.org/rss/1.0/modules/syndication/'}
|
||||
|
||||
def setUp(self):
|
||||
self.fg = FeedGenerator()
|
||||
self.fg.load_extension('syndication')
|
||||
|
@ -20,14 +22,11 @@ class TestExtensionSyndication(unittest.TestCase):
|
|||
self.fg.description('description')
|
||||
|
||||
def test_update_period(self):
|
||||
for period_type in ('hourly', 'daily', 'weekly',
|
||||
'monthly', 'yearly'):
|
||||
for period_type in ('hourly', 'daily', 'weekly', 'monthly', 'yearly'):
|
||||
self.fg.syndication.update_period(period_type)
|
||||
root = etree.fromstring(self.fg.rss_str())
|
||||
a = root.xpath('/rss/channel/sy:UpdatePeriod',
|
||||
namespaces={
|
||||
'sy':'http://purl.org/rss/1.0/modules/syndication/'
|
||||
})
|
||||
namespaces=self.SYN_NS)
|
||||
assert a[0].text == period_type
|
||||
|
||||
def test_update_frequency(self):
|
||||
|
@ -35,19 +34,14 @@ class TestExtensionSyndication(unittest.TestCase):
|
|||
self.fg.syndication.update_frequency(frequency)
|
||||
root = etree.fromstring(self.fg.rss_str())
|
||||
a = root.xpath('/rss/channel/sy:UpdateFrequency',
|
||||
namespaces={
|
||||
'sy':'http://purl.org/rss/1.0/modules/syndication/'
|
||||
})
|
||||
namespaces=self.SYN_NS)
|
||||
assert a[0].text == str(frequency)
|
||||
|
||||
def test_update_base(self):
|
||||
base = '2000-01-01T12:00+00:00'
|
||||
self.fg.syndication.update_base(base)
|
||||
root = etree.fromstring(self.fg.rss_str())
|
||||
a = root.xpath('/rss/channel/sy:UpdateBase',
|
||||
namespaces={
|
||||
'sy':'http://purl.org/rss/1.0/modules/syndication/'
|
||||
})
|
||||
a = root.xpath('/rss/channel/sy:UpdateBase', namespaces=self.SYN_NS)
|
||||
assert a[0].text == base
|
||||
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ from lxml import etree
|
|||
from feedgen.feed import FeedGenerator
|
||||
from feedgen.ext.dc import DcExtension, DcEntryExtension
|
||||
|
||||
|
||||
class TestSequenceFunctions(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
@ -50,12 +51,14 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
self.cloudProtocol = 'SOAP 1.1'
|
||||
|
||||
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'}
|
||||
self.copyright = "The copyright notice"
|
||||
self.docs = 'http://www.rssboard.org/rss-specification'
|
||||
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.skipHours = 23
|
||||
|
||||
|
@ -77,7 +80,8 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
fg.link(href=self.link2Href, rel=self.link2Rel)
|
||||
fg.language(self.language)
|
||||
fg.cloud(domain=self.cloudDomain, port=self.cloudPort,
|
||||
path=self.cloudPath, registerProcedure=self.cloudRegisterProcedure,
|
||||
path=self.cloudPath,
|
||||
registerProcedure=self.cloudRegisterProcedure,
|
||||
protocol=self.cloudProtocol)
|
||||
fg.icon(self.icon)
|
||||
fg.category(term=self.categoryTerm, scheme=self.categoryScheme,
|
||||
|
@ -90,14 +94,13 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
fg.skipDays(self.skipDays)
|
||||
fg.skipHours(self.skipHours)
|
||||
fg.textInput(title=self.textInputTitle,
|
||||
description=self.textInputDescription, name=self.textInputName,
|
||||
link=self.textInputLink)
|
||||
description=self.textInputDescription,
|
||||
name=self.textInputName, link=self.textInputLink)
|
||||
fg.ttl(self.ttl)
|
||||
fg.webMaster(self.webMaster)
|
||||
|
||||
self.fg = fg
|
||||
|
||||
|
||||
def test_baseFeed(self):
|
||||
fg = self.fg
|
||||
|
||||
|
@ -137,18 +140,22 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
def test_rel_values_for_atom(self):
|
||||
values_for_rel = [
|
||||
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark',
|
||||
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form',
|
||||
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure',
|
||||
'duplicate', 'edit', 'edit-form', 'edit-media', 'enclosure', 'first', 'glossary',
|
||||
'help', 'hosts', 'hub', 'icon', 'index', 'item', 'last', 'latest-version', 'license',
|
||||
'lrdd', 'memento', 'monitor', 'monitor-group', 'next', 'next-archive', 'nofollow',
|
||||
'noreferrer', 'original', 'payment', 'predecessor-version', 'prefetch', 'prev', 'preview',
|
||||
'previous', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies', 'search',
|
||||
'section', 'self', 'service', 'start', 'stylesheet', 'subsection', 'successor-version',
|
||||
'tag', 'terms-of-service', 'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
|
||||
'working-copy', 'working-copy-of'
|
||||
]
|
||||
links = [{'href': '%s/%s' % (self.linkHref, val.replace('-', '_')), 'rel': val} for val in values_for_rel]
|
||||
'canonical', 'chapter', 'collection', 'contents', 'copyright',
|
||||
'create-form', 'current', 'derivedfrom', 'describedby',
|
||||
'describes', 'disclosure', 'duplicate', 'edit', 'edit-form',
|
||||
'edit-media', 'enclosure', 'first', 'glossary', 'help', 'hosts',
|
||||
'hub', 'icon', 'index', 'item', 'last', 'latest-version',
|
||||
'license', 'lrdd', 'memento', 'monitor', 'monitor-group', 'next',
|
||||
'next-archive', 'nofollow', 'noreferrer', 'original', 'payment',
|
||||
'predecessor-version', 'prefetch', 'prev', 'preview', 'previous',
|
||||
'prev-archive', 'privacy-policy', 'profile', 'related', 'replies',
|
||||
'search', 'section', 'self', 'service', 'start', 'stylesheet',
|
||||
'subsection', 'successor-version', 'tag', 'terms-of-service',
|
||||
'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
|
||||
'working-copy', 'working-copy-of']
|
||||
links = [{'href': '%s/%s' % (self.linkHref,
|
||||
val.replace('-', '_')), 'rel': val}
|
||||
for val in values_for_rel]
|
||||
fg = self.fg
|
||||
fg.link(links, replace=True)
|
||||
atomString = fg.atom_str(pretty=True, xml_declaration=False)
|
||||
|
@ -165,18 +172,22 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
def test_rel_values_for_rss(self):
|
||||
values_for_rel = [
|
||||
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark',
|
||||
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form',
|
||||
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure',
|
||||
'duplicate', 'edit', 'edit-form', 'edit-media', 'enclosure', 'first', 'glossary',
|
||||
'help', 'hosts', 'hub', 'icon', 'index', 'item', 'last', 'latest-version', 'license',
|
||||
'lrdd', 'memento', 'monitor', 'monitor-group', 'next', 'next-archive', 'nofollow',
|
||||
'noreferrer', 'original', 'payment', 'predecessor-version', 'prefetch', 'prev', 'preview',
|
||||
'previous', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies', 'search',
|
||||
'section', 'self', 'service', 'start', 'stylesheet', 'subsection', 'successor-version',
|
||||
'tag', 'terms-of-service', 'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
|
||||
'working-copy', 'working-copy-of'
|
||||
]
|
||||
links = [{'href': '%s/%s' % (self.linkHref, val.replace('-', '_')), 'rel': val} for val in values_for_rel]
|
||||
'canonical', 'chapter', 'collection', 'contents', 'copyright',
|
||||
'create-form', 'current', 'derivedfrom', 'describedby',
|
||||
'describes', 'disclosure', 'duplicate', 'edit', 'edit-form',
|
||||
'edit-media', 'enclosure', 'first', 'glossary', 'help', 'hosts',
|
||||
'hub', 'icon', 'index', 'item', 'last', 'latest-version',
|
||||
'license', 'lrdd', 'memento', 'monitor', 'monitor-group', 'next',
|
||||
'next-archive', 'nofollow', 'noreferrer', 'original', 'payment',
|
||||
'predecessor-version', 'prefetch', 'prev', 'preview', 'previous',
|
||||
'prev-archive', 'privacy-policy', 'profile', 'related', 'replies',
|
||||
'search', 'section', 'self', 'service', 'start', 'stylesheet',
|
||||
'subsection', 'successor-version', 'tag', 'terms-of-service',
|
||||
'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
|
||||
'working-copy', 'working-copy-of']
|
||||
links = [{'href': '%s/%s' % (self.linkHref,
|
||||
val.replace('-', '_')), 'rel': val}
|
||||
for val in values_for_rel]
|
||||
fg = self.fg
|
||||
fg.link(links, replace=True)
|
||||
rssString = fg.rss_str(pretty=True, xml_declaration=False)
|
||||
|
@ -185,13 +196,16 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
nsAtom = self.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('rel') == 'self'
|
||||
|
||||
rss_links = channel.findall('link')
|
||||
assert len(rss_links) == 1 # 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('-', '_'))
|
||||
# RSS only needs one URL. We use the first link for RSS:
|
||||
assert len(rss_links) == 1
|
||||
assert rss_links[0].text == '%s/%s' % \
|
||||
(self.linkHref, 'working-copy-of'.replace('-', '_'))
|
||||
|
||||
def checkAtomString(self, atomString):
|
||||
|
||||
|
@ -200,22 +214,31 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
nsAtom = self.nsAtom
|
||||
|
||||
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}category" % nsAtom).get('term') == self.categoryTerm
|
||||
assert feed.find("{%s}category" % nsAtom).get('label') == self.categoryLabel
|
||||
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.find("{%s}category" % nsAtom)\
|
||||
.get('term') == self.categoryTerm
|
||||
assert feed.find("{%s}category" % nsAtom)\
|
||||
.get('label') == self.categoryLabel
|
||||
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)[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.find("{%s}logo" % nsAtom).text == self.logo
|
||||
assert feed.find("{%s}icon" % nsAtom).text == self.icon
|
||||
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).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}contributor" % nsAtom)\
|
||||
.find("{%s}name" % nsAtom).text == self.contributor['name']
|
||||
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
|
||||
|
||||
def test_rssFeedFile(self):
|
||||
|
@ -248,7 +271,7 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
def test_extensionAlreadyLoaded(self):
|
||||
fg = self.fg
|
||||
fg.load_extension('dc', atom=True, rss=True)
|
||||
with self.assertRaises(ImportError) as context:
|
||||
with self.assertRaises(ImportError):
|
||||
fg.load_extension('dc')
|
||||
|
||||
def test_registerCustomExtension(self):
|
||||
|
@ -261,39 +284,42 @@ class TestSequenceFunctions(unittest.TestCase):
|
|||
|
||||
feed = etree.fromstring(rssString)
|
||||
nsAtom = self.nsAtom
|
||||
nsRss = self.nsRss
|
||||
|
||||
channel = feed.find("channel")
|
||||
assert channel != None
|
||||
ch = feed.find("channel")
|
||||
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__':
|
||||
unittest.main()
|
||||
|
|
Loading…
Reference in a new issue