Flake8 Compatibility

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

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

View file

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

View file

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

View file

@ -1,136 +1,137 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
======= =======
feedgen feedgen
======= =======
This module can be used to generate web feeds in both ATOM and RSS format. This module can be used to generate web feeds in both ATOM and RSS format.
It has support for extensions. Included is for example an extension to It has support for extensions. Included is for example an extension to
produce Podcasts. produce Podcasts.
:copyright: 2013 by Lars Kiesow :copyright: 2013 by Lars Kiesow
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
------------- -------------
Create a Feed Create a Feed
------------- -------------
To create a feed simply instanciate the FeedGenerator class and insert some To create a feed simply instanciate the FeedGenerator class and insert some
data:: data::
>>> from feedgen.feed import FeedGenerator >>> from feedgen.feed import FeedGenerator
>>> fg = FeedGenerator() >>> fg = FeedGenerator()
>>> fg.id('http://lernfunk.de/media/654321') >>> fg.id('http://lernfunk.de/media/654321')
>>> fg.title('Some Testfeed') >>> fg.title('Some Testfeed')
>>> fg.author( {'name':'John Doe','email':'john@example.de'} ) >>> fg.author( {'name':'John Doe','email':'john@example.de'} )
>>> fg.link( href='http://example.com', rel='alternate' ) >>> fg.link( href='http://example.com', rel='alternate' )
>>> fg.logo('http://ex.com/logo.jpg') >>> fg.logo('http://ex.com/logo.jpg')
>>> fg.subtitle('This is a cool feed!') >>> fg.subtitle('This is a cool feed!')
>>> fg.link( href='http://larskiesow.de/test.atom', rel='self' ) >>> fg.link( href='http://larskiesow.de/test.atom', rel='self' )
>>> fg.language('en') >>> fg.language('en')
Note that for the methods which set fields that can occur more than once in Note that for the methods which set fields that can occur more than once in
a feed you can use all of the following ways to provide data: a feed you can use all of the following ways to provide data:
- Provide the data for that element as keyword arguments - Provide the data for that element as keyword arguments
- Provide the data for that element as dictionary - Provide the data for that element as dictionary
- Provide a list of dictionaries with the data for several elements - Provide a list of dictionaries with the data for several elements
Example:: Example::
>>> fg.contributor( name='John Doe', email='jdoe@example.com' ) >>> fg.contributor(name='John Doe', email='jdoe@example.com' )
>>> fg.contributor({'name':'John Doe', 'email':'jdoe@example.com'}) >>> fg.contributor({'name':'John Doe', 'email':'jdoe@example.com'})
>>> fg.contributor([{'name':'John Doe', 'email':'jdoe@example.com'}, ...]) >>> fg.contributor([{'name':'John', 'email':'jdoe@example.com'}, ])
----------------- -----------------
Generate the Feed Generate the Feed
----------------- -----------------
After that you can generate both RSS or ATOM by calling the respective method:: After that you can generate both RSS or ATOM by calling the respective
method::
>>> atomfeed = fg.atom_str(pretty=True) # Get the ATOM feed as string >>> atomfeed = fg.atom_str(pretty=True) # Get the ATOM feed as string
>>> rssfeed = fg.rss_str(pretty=True) # Get the RSS feed as string >>> rssfeed = fg.rss_str(pretty=True) # Get the RSS feed as string
>>> fg.atom_file('atom.xml') # Write the ATOM feed to a file >>> fg.atom_file('atom.xml') # Write the ATOM feed to a file
>>> fg.rss_file('rss.xml') # Write the RSS feed to a file >>> fg.rss_file('rss.xml') # Write the RSS feed to a file
---------------- ----------------
Add Feed Entries Add Feed Entries
---------------- ----------------
To add entries (items) to a feed you need to create new FeedEntry objects To add entries (items) to a feed you need to create new FeedEntry objects
and append them to the list of entries in the FeedGenerator. The most and append them to the list of entries in the FeedGenerator. The most
convenient way to go is to use the FeedGenerator itself for the convenient way to go is to use the FeedGenerator itself for the
instantiation of the FeedEntry object:: instantiation of the FeedEntry object::
>>> fe = fg.add_entry() >>> fe = fg.add_entry()
>>> fe.id('http://lernfunk.de/media/654321/1') >>> fe.id('http://lernfunk.de/media/654321/1')
>>> fe.title('The First Episode') >>> fe.title('The First Episode')
The FeedGenerators method add_entry(...) without argument provides will The FeedGenerators method add_entry(...) without argument provides will
automatically generate a new FeedEntry object, append it to the feeds automatically generate a new FeedEntry object, append it to the feeds
internal list of entries and return it, so that additional data can be internal list of entries and return it, so that additional data can be
added. added.
---------- ----------
Extensions Extensions
---------- ----------
The FeedGenerator supports extension to include additional data into the XML The FeedGenerator supports extension to include additional data into the
structure of the feeds. Extensions can be loaded like this:: XML structure of the feeds. Extensions can be loaded like this::
>>> fg.load_extension('someext', atom=True, rss=True) >>> fg.load_extension('someext', atom=True, rss=True)
This will try to load the extension someext from the file This will try to load the extension someext from the file
`ext/someext.py`. It is required that `someext.py` contains a class named `ext/someext.py`. It is required that `someext.py` contains a class named
SomextExtension which is required to have at least the two methods SomextExtension which is required to have at least the two methods
`extend_rss(...)` and `extend_atom(...)`. Although not required, it is `extend_rss(...)` and `extend_atom(...)`. Although not required, it is
strongly suggested to use BaseExtension from `ext/base.py` as superclass. strongly suggested to use BaseExtension from `ext/base.py` as superclass.
`load_extension('someext', ...)` will also try to load a class named `load_extension('someext', ...)` will also try to load a class named
SomextEntryExtension for every entry of the feed. This class can be SomextEntryExtension for every entry of the feed. This class can be
located either in the same file as SomextExtension or in located either in the same file as SomextExtension or in
`ext/someext_entry.py` which is suggested especially for large extensions. `ext/someext_entry.py` which is suggested especially for large extensions.
The parameters `atom` and `rss` tell the FeedGenerator if the extensions The parameters `atom` and `rss` tell the FeedGenerator if the extensions
should only be used for either ATOM or RSS feeds. The default value for both should only be used for either ATOM or RSS feeds. The default value for
parameters is true which means that the extension would be used for both both parameters is true which means that the extension would be used for
kinds of feeds. both kinds of feeds.
**Example: Produceing a Podcast** **Example: Produceing a Podcast**
One extension already provided is the podcast extension. A podcast is an RSS One extension already provided is the podcast extension. A podcast is an
feed with some additional elements for ITunes. RSS feed with some additional elements for ITunes.
To produce a podcast simply load the `podcast` extension:: To produce a podcast simply load the `podcast` extension::
>>> from feedgen.feed import FeedGenerator >>> from feedgen.feed import FeedGenerator
>>> fg = FeedGenerator() >>> fg = FeedGenerator()
>>> fg.load_extension('podcast') >>> fg.load_extension('podcast')
... ...
>>> fg.podcast.itunes_category('Technology', 'Podcasting') >>> fg.podcast.itunes_category('Technology', 'Podcasting')
... ...
>>> fg.rss_str(pretty=True) >>> fg.rss_str(pretty=True)
>>> fg.rss_file('podcast.xml') >>> fg.rss_file('podcast.xml')
Of cause the extension has to be loaded for the FeedEntry objects as well Of cause the extension has to be loaded for the FeedEntry objects as well
but this is done automatically by the FeedGenerator for every feed entry if but this is done automatically by the FeedGenerator for every feed entry if
the extension is loaded for the whole feed. You can, however, load an the extension is loaded for the whole feed. You can, however, load an
extension for a specific FeedEntry by calling `load_extension(...)` on that extension for a specific FeedEntry by calling `load_extension(...)` on that
entry. But this is a rather uncommon use. entry. But this is a rather uncommon use.
Of cause you can still produce a normal ATOM or RSS feed, even if you have Of cause you can still produce a normal ATOM or RSS feed, even if you have
loaded some plugins by temporary disabling them during the feed generation. loaded some plugins by temporary disabling them during the feed generation.
This can be done by calling the generating method with the keyword argument This can be done by calling the generating method with the keyword argument
`extensions` set to `False`. `extensions` set to `False`.
--------------------- ---------------------
Testing the Generator Testing the Generator
--------------------- ---------------------
You can test the module by simply executing:: You can test the module by simply executing::
$ python -m feedgen $ python -m feedgen
""" """

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,43 +1,43 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
feedgen.ext.base feedgen.ext.base
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
Basic FeedGenerator extension which does nothing but provides all necessary Basic FeedGenerator extension which does nothing but provides all necessary
methods. methods.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
class BaseExtension(object): class BaseExtension(object):
'''Basic FeedGenerator extension. '''Basic FeedGenerator extension.
''' '''
def extend_ns(self): def extend_ns(self):
'''Returns a dict that will be used in the namespace map for the feed.''' '''Returns a dict that will be used in the namespace map for the feed.
return dict() '''
return dict()
def extend_rss(self, feed): def extend_rss(self, feed):
'''Extend a RSS feed xml structure containing all previously set fields. '''Extend a RSS feed xml structure containing all previously set fields.
:param feed: The feed xml root element. :param feed: The feed xml root element.
:returns: The feed root element. :returns: The feed root element.
''' '''
return feed return feed
def extend_atom(self, feed):
'''Extend an ATOM feed xml structure containing all previously set
fields.
def extend_atom(self, feed): :param feed: The feed xml root element.
'''Extend an ATOM feed xml structure containing all previously set :returns: The feed root element.
fields. '''
return feed
:param feed: The feed xml root element.
:returns: The feed root element.
'''
return feed
class BaseEntryExtension(BaseExtension): class BaseEntryExtension(BaseExtension):
'''Basic FeedEntry extension. '''Basic FeedEntry extension.
''' '''

View file

@ -1,419 +1,406 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
feedgen.ext.dc feedgen.ext.dc
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to add Dubline Core Elements to the feeds. Extends the FeedGenerator to add Dubline Core Elements to the feeds.
Descriptions partly taken from Descriptions partly taken from
http://dublincore.org/documents/dcmi-terms/#elements-coverage http://dublincore.org/documents/dcmi-terms/#elements-coverage
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013-2016, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
from lxml import etree from lxml import etree
from feedgen.ext.base import BaseExtension, BaseEntryExtension from feedgen.ext.base import BaseExtension
class DcBaseExtension(BaseExtension): class DcBaseExtension(BaseExtension):
'''Dublin Core Elements extension for podcasts. '''Dublin Core Elements extension for podcasts.
''' '''
def __init__(self):
def __init__(self): # http://dublincore.org/documents/usageguide/elements.shtml
# http://dublincore.org/documents/usageguide/elements.shtml # http://dublincore.org/documents/dces/
# http://dublincore.org/documents/dces/ # http://dublincore.org/documents/dcmi-terms/
# http://dublincore.org/documents/dcmi-terms/ self._dcelem_contributor = None
self._dcelem_contributor = None self._dcelem_coverage = None
self._dcelem_coverage = None self._dcelem_creator = None
self._dcelem_creator = None self._dcelem_date = None
self._dcelem_date = None self._dcelem_description = None
self._dcelem_description = None self._dcelem_format = None
self._dcelem_format = None self._dcelem_identifier = None
self._dcelem_identifier = None self._dcelem_language = None
self._dcelem_language = None self._dcelem_publisher = None
self._dcelem_publisher = None self._dcelem_relation = None
self._dcelem_relation = None self._dcelem_rights = None
self._dcelem_rights = None self._dcelem_source = None
self._dcelem_source = None self._dcelem_subject = None
self._dcelem_subject = None self._dcelem_title = None
self._dcelem_title = None self._dcelem_type = None
self._dcelem_type = None
def extend_ns(self):
def extend_ns(self): return {'dc': 'http://purl.org/dc/elements/1.1/'}
return {'dc' : 'http://purl.org/dc/elements/1.1/'}
def _extend_xml(self, xml_elem):
def _extend_xml(self, xml_elem): '''Extend xml_elem with set DC fields.
'''Extend xml_elem with set DC fields.
:param xml_elem: etree element
:param xml_elem: etree element '''
''' DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/'
DCELEMENTS_NS = 'http://purl.org/dc/elements/1.1/'
for elem in ['contributor', 'coverage', 'creator', 'date',
for elem in ['contributor', 'coverage', 'creator', 'date', 'description', 'description', 'language', 'publisher', 'relation',
'language', 'publisher', 'relation', 'rights', 'source', 'subject', 'rights', 'source', 'subject', 'title', 'type', 'format',
'title', 'type', 'format', 'identifier']: 'identifier']:
if hasattr(self, '_dcelem_%s' % elem): if hasattr(self, '_dcelem_%s' % elem):
for val in getattr(self, '_dcelem_%s' % elem) or []: for val in getattr(self, '_dcelem_%s' % elem) or []:
node = etree.SubElement(xml_elem, '{%s}%s' % (DCELEMENTS_NS, elem)) node = etree.SubElement(xml_elem,
node.text = val '{%s}%s' % (DCELEMENTS_NS, elem))
node.text = val
def extend_atom(self, atom_feed): def extend_atom(self, atom_feed):
'''Extend an Atom feed with the set DC fields. '''Extend an Atom feed with the set DC fields.
:param atom_feed: The feed root element :param atom_feed: The feed root element
:returns: The feed root element :returns: The feed root element
''' '''
self._extend_xml(atom_feed) self._extend_xml(atom_feed)
return atom_feed return atom_feed
def extend_rss(self, rss_feed):
'''Extend a RSS feed with the set DC fields.
def extend_rss(self, rss_feed):
'''Extend a RSS feed with the set DC fields. :param rss_feed: The feed root element
:returns: The feed root element.
:param rss_feed: The feed root element '''
:returns: The feed root element. channel = rss_feed[0]
''' self._extend_xml(channel)
channel = rss_feed[0]
self._extend_xml(channel) return rss_feed
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.
def dc_contributor(self, contributor=None, replace=False):
'''Get or set the dc:contributor which is an entity responsible for For more information see:
making contributions to the resource. http://dublincore.org/documents/dcmi-terms/#elements-contributor
For more information see: :param contributor: Contributor or list of contributors.
http://dublincore.org/documents/dcmi-terms/#elements-contributor :param replace: Replace alredy set contributors (deault: False).
:returns: List of contributors.
:param contributor: Contributor or list of contributors. '''
:param replace: Replace alredy set contributors (deault: False). if contributor is not None:
:returns: List of contributors. if not isinstance(contributor, list):
''' contributor = [contributor]
if not contributor is None: if replace or not self._dcelem_contributor:
if not isinstance(contributor, list): self._dcelem_contributor = []
contributor = [contributor] self._dcelem_contributor += contributor
if replace or not self._dcelem_contributor: return self._dcelem_contributor
self._dcelem_contributor = []
self._dcelem_contributor += contributor def dc_coverage(self, coverage=None, replace=True):
return self._dcelem_contributor '''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.
def dc_coverage(self, coverage=None, replace=True):
'''Get or set the dc:coverage which indicated the spatial or temporal Spatial topic and spatial applicability may be a named place or a
topic of the resource, the spatial applicability of the resource, or the location specified by its geographic coordinates. Temporal topic may be
jurisdiction under which the resource is relevant. a named period, date, or date range. A jurisdiction may be a named
administrative entity or a geographic place to which the resource
Spatial topic and spatial applicability may be a named place or a applies. Recommended best practice is to use a controlled vocabulary
location specified by its geographic coordinates. Temporal topic may be a such as the Thesaurus of Geographic Names [TGN]. Where appropriate,
named period, date, or date range. A jurisdiction may be a named named places or time periods can be used in preference to numeric
administrative entity or a geographic place to which the resource 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 References:
places or time periods can be used in preference to numeric identifiers [TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html
such as sets of coordinates or date ranges.
:param coverage: Coverage of the feed.
References: [TGN] http://www.getty.edu/research/tools/vocabulary/tgn/index.html :param replace: Replace already set coverage (default: True).
:returns: Coverage of the feed.
:param coverage: Coverage of the feed. '''
:param replace: Replace already set coverage (default: True). if coverage is not None:
:returns: Coverage of the feed. if not isinstance(coverage, list):
''' coverage = [coverage]
if not coverage is None: if replace or not self._dcelem_coverage:
if not isinstance(coverage, list): self._dcelem_coverage = []
coverage = [coverage] self._dcelem_coverage = coverage
if replace or not self._dcelem_coverage: return self._dcelem_coverage
self._dcelem_coverage = []
self._dcelem_coverage = coverage def dc_creator(self, creator=None, replace=False):
return self._dcelem_coverage '''Get or set the dc:creator which is an entity primarily responsible
for making the resource.
def dc_creator(self, creator=None, replace=False): For more information see:
'''Get or set the dc:creator which is an entity primarily responsible for http://dublincore.org/documents/dcmi-terms/#elements-creator
making the resource.
:param creator: Creator or list of creators.
For more information see: :param replace: Replace alredy set creators (deault: False).
http://dublincore.org/documents/dcmi-terms/#elements-creator :returns: List of creators.
'''
:param creator: Creator or list of creators. if creator is not None:
:param replace: Replace alredy set creators (deault: False). if not isinstance(creator, list):
:returns: List of creators. creator = [creator]
''' if replace or not self._dcelem_creator:
if not creator is None: self._dcelem_creator = []
if not isinstance(creator, list): self._dcelem_creator += creator
creator = [creator] return self._dcelem_creator
if replace or not self._dcelem_creator:
self._dcelem_creator = [] def dc_date(self, date=None, replace=True):
self._dcelem_creator += creator '''Get or set the dc:date which describes a point or period of time
return self._dcelem_creator associated with an event in the lifecycle of the resource.
For more information see:
def dc_date(self, date=None, replace=True): http://dublincore.org/documents/dcmi-terms/#elements-date
'''Get or set the dc:date which describes a point or period of time
associated with an event in the lifecycle of the resource. :param date: Date or list of dates.
:param replace: Replace alredy set dates (deault: True).
For more information see: :returns: List of dates.
http://dublincore.org/documents/dcmi-terms/#elements-date '''
if date is not None:
:param date: Date or list of dates. if not isinstance(date, list):
:param replace: Replace alredy set dates (deault: True). date = [date]
:returns: List of dates. if replace or not self._dcelem_date:
''' self._dcelem_date = []
if not date is None: self._dcelem_date += date
if not isinstance(date, list): return self._dcelem_date
date = [date]
if replace or not self._dcelem_date: def dc_description(self, description=None, replace=True):
self._dcelem_date = [] '''Get or set the dc:description which is an account of the resource.
self._dcelem_date += date
return self._dcelem_date For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-description
def dc_description(self, description=None, replace=True): :param description: Description or list of descriptions.
'''Get or set the dc:description which is an account of the resource. :param replace: Replace alredy set descriptions (deault: True).
:returns: List of descriptions.
For more information see: '''
http://dublincore.org/documents/dcmi-terms/#elements-description if description is not None:
if not isinstance(description, list):
:param description: Description or list of descriptions. description = [description]
:param replace: Replace alredy set descriptions (deault: True). if replace or not self._dcelem_description:
:returns: List of descriptions. self._dcelem_description = []
''' self._dcelem_description += description
if not description is None: return self._dcelem_description
if not isinstance(description, list):
description = [description] def dc_format(self, format=None, replace=True):
if replace or not self._dcelem_description: '''Get or set the dc:format which describes the file format, physical
self._dcelem_description = [] medium, or dimensions of the resource.
self._dcelem_description += description
return self._dcelem_description For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-format
def dc_format(self, format=None, replace=True): :param format: Format of the resource or list of formats.
'''Get or set the dc:format which describes the file format, physical :param replace: Replace alredy set format (deault: True).
medium, or dimensions of the resource. :returns: Format of the resource.
'''
For more information see: if format is not None:
http://dublincore.org/documents/dcmi-terms/#elements-format if not isinstance(format, list):
format = [format]
:param format: Format of the resource or list of formats. if replace or not self._dcelem_format:
:param replace: Replace alredy set format (deault: True). self._dcelem_format = []
:returns: Format of the resource. self._dcelem_format += format
''' return self._dcelem_format
if not format is None:
if not isinstance(format, list): def dc_identifier(self, identifier=None, replace=True):
format = [format] '''Get or set the dc:identifier which should be an unambiguous
if replace or not self._dcelem_format: reference to the resource within a given context.
self._dcelem_format = []
self._dcelem_format += format For more inidentifierion see:
return self._dcelem_format http://dublincore.org/documents/dcmi-terms/#elements-identifier
:param identifier: Identifier of the resource or list of identifiers.
def dc_identifier(self, identifier=None, replace=True): :param replace: Replace alredy set identifier (deault: True).
'''Get or set the dc:identifier which should be an unambiguous reference :returns: Identifiers of the resource.
to the resource within a given context. '''
if identifier is not None:
For more inidentifierion see: if not isinstance(identifier, list):
http://dublincore.org/documents/dcmi-terms/#elements-identifier identifier = [identifier]
if replace or not self._dcelem_identifier:
:param identifier: Identifier of the resource or list of identifiers. self._dcelem_identifier = []
:param replace: Replace alredy set identifier (deault: True). self._dcelem_identifier += identifier
:returns: Identifiers of the resource.
''' def dc_language(self, language=None, replace=True):
if not identifier is None: '''Get or set the dc:language which describes a language of the
if not isinstance(identifier, list): resource.
identifier = [identifier]
if replace or not self._dcelem_identifier: For more information see:
self._dcelem_identifier = [] http://dublincore.org/documents/dcmi-terms/#elements-language
self._dcelem_identifier += identifier
:param language: Language or list of languages.
:param replace: Replace alredy set languages (deault: True).
def dc_language(self, language=None, replace=True): :returns: List of languages.
'''Get or set the dc:language which describes a language of the resource. '''
if language is not None:
For more information see: if not isinstance(language, list):
http://dublincore.org/documents/dcmi-terms/#elements-language language = [language]
if replace or not self._dcelem_language:
:param language: Language or list of languages. self._dcelem_language = []
:param replace: Replace alredy set languages (deault: True). self._dcelem_language += language
:returns: List of languages. return self._dcelem_language
'''
if not language is None: def dc_publisher(self, publisher=None, replace=False):
if not isinstance(language, list): '''Get or set the dc:publisher which is an entity responsible for
language = [language] making the resource available.
if replace or not self._dcelem_language:
self._dcelem_language = [] For more information see:
self._dcelem_language += language http://dublincore.org/documents/dcmi-terms/#elements-publisher
return self._dcelem_language
:param publisher: Publisher or list of publishers.
:param replace: Replace alredy set publishers (deault: False).
def dc_publisher(self, publisher=None, replace=False): :returns: List of publishers.
'''Get or set the dc:publisher which is an entity responsible for making '''
the resource available. if publisher is not None:
if not isinstance(publisher, list):
For more information see: publisher = [publisher]
http://dublincore.org/documents/dcmi-terms/#elements-publisher if replace or not self._dcelem_publisher:
self._dcelem_publisher = []
:param publisher: Publisher or list of publishers. self._dcelem_publisher += publisher
:param replace: Replace alredy set publishers (deault: False). return self._dcelem_publisher
:returns: List of publishers.
''' def dc_relation(self, relation=None, replace=False):
if not publisher is None: '''Get or set the dc:relation which describes a related ressource.
if not isinstance(publisher, list):
publisher = [publisher] For more information see:
if replace or not self._dcelem_publisher: http://dublincore.org/documents/dcmi-terms/#elements-relation
self._dcelem_publisher = []
self._dcelem_publisher += publisher :param relation: Relation or list of relations.
return self._dcelem_publisher :param replace: Replace alredy set relations (deault: False).
:returns: List of relations.
'''
def dc_relation(self, relation=None, replace=False): if relation is not None:
'''Get or set the dc:relation which describes a related ressource. if not isinstance(relation, list):
relation = [relation]
For more information see: if replace or not self._dcelem_relation:
http://dublincore.org/documents/dcmi-terms/#elements-relation self._dcelem_relation = []
self._dcelem_relation += relation
:param relation: Relation or list of relations. return self._dcelem_relation
:param replace: Replace alredy set relations (deault: False).
:returns: List of relations. def dc_rights(self, rights=None, replace=False):
''' '''Get or set the dc:rights which may contain information about rights
if not relation is None: held in and over the resource.
if not isinstance(relation, list):
relation = [relation] For more information see:
if replace or not self._dcelem_relation: http://dublincore.org/documents/dcmi-terms/#elements-rights
self._dcelem_relation = []
self._dcelem_relation += relation :param rights: Rights information or list of rights information.
return self._dcelem_relation :param replace: Replace alredy set rightss (deault: False).
:returns: List of rights information.
'''
def dc_rights(self, rights=None, replace=False): if rights is not None:
'''Get or set the dc:rights which may contain information about rights if not isinstance(rights, list):
held in and over the resource. rights = [rights]
if replace or not self._dcelem_rights:
For more information see: self._dcelem_rights = []
http://dublincore.org/documents/dcmi-terms/#elements-rights self._dcelem_rights += rights
return self._dcelem_rights
:param rights: Rights information or list of rights information.
:param replace: Replace alredy set rightss (deault: False). def dc_source(self, source=None, replace=False):
:returns: List of rights information. '''Get or set the dc:source which is a related resource from which the
''' described resource is derived.
if not rights is None:
if not isinstance(rights, list): The described resource may be derived from the related resource in
rights = [rights] whole or in part. Recommended best practice is to identify the related
if replace or not self._dcelem_rights: resource by means of a string conforming to a formal identification
self._dcelem_rights = [] system.
self._dcelem_rights += rights
return self._dcelem_rights For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-source
def dc_source(self, source=None, replace=False): :param source: Source or list of sources.
'''Get or set the dc:source which is a related resource from which the :param replace: Replace alredy set sources (deault: False).
described resource is derived. :returns: List of sources.
'''
The described resource may be derived from the related resource in whole if source is not None:
or in part. Recommended best practice is to identify the related resource if not isinstance(source, list):
by means of a string conforming to a formal identification system. source = [source]
if replace or not self._dcelem_source:
self._dcelem_source = []
For more information see: self._dcelem_source += source
http://dublincore.org/documents/dcmi-terms/#elements-source return self._dcelem_source
:param source: Source or list of sources. def dc_subject(self, subject=None, replace=False):
:param replace: Replace alredy set sources (deault: False). '''Get or set the dc:subject which describes the topic of the resource.
:returns: List of sources.
''' For more information see:
if not source is None: http://dublincore.org/documents/dcmi-terms/#elements-subject
if not isinstance(source, list):
source = [source] :param subject: Subject or list of subjects.
if replace or not self._dcelem_source: :param replace: Replace alredy set subjects (deault: False).
self._dcelem_source = [] :returns: List of subjects.
self._dcelem_source += source '''
return self._dcelem_source if subject is not None:
if not isinstance(subject, list):
subject = [subject]
def dc_subject(self, subject=None, replace=False): if replace or not self._dcelem_subject:
'''Get or set the dc:subject which describes the topic of the resource. self._dcelem_subject = []
self._dcelem_subject += subject
For more information see: return self._dcelem_subject
http://dublincore.org/documents/dcmi-terms/#elements-subject
def dc_title(self, title=None, replace=True):
:param subject: Subject or list of subjects. '''Get or set the dc:title which is a name given to the resource.
:param replace: Replace alredy set subjects (deault: False).
:returns: List of subjects. For more information see:
''' http://dublincore.org/documents/dcmi-terms/#elements-title
if not subject is None:
if not isinstance(subject, list): :param title: Title or list of titles.
subject = [subject] :param replace: Replace alredy set titles (deault: False).
if replace or not self._dcelem_subject: :returns: List of titles.
self._dcelem_subject = [] '''
self._dcelem_subject += subject if title is not None:
return self._dcelem_subject if not isinstance(title, list):
title = [title]
if replace or not self._dcelem_title:
def dc_title(self, title=None, replace=True): self._dcelem_title = []
'''Get or set the dc:title which is a name given to the resource. self._dcelem_title += title
return self._dcelem_title
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-title def dc_type(self, type=None, replace=False):
'''Get or set the dc:type which describes the nature or genre of the
:param title: Title or list of titles. resource.
:param replace: Replace alredy set titles (deault: False).
:returns: List of titles. For more information see:
''' http://dublincore.org/documents/dcmi-terms/#elements-type
if not title is None:
if not isinstance(title, list): :param type: Type or list of types.
title = [title] :param replace: Replace alredy set types (deault: False).
if replace or not self._dcelem_title: :returns: List of types.
self._dcelem_title = [] '''
self._dcelem_title += title if type is not None:
return self._dcelem_title if not isinstance(type, list):
type = [type]
if replace or not self._dcelem_type:
def dc_type(self, type=None, replace=False): self._dcelem_type = []
'''Get or set the dc:type which describes the nature or genre of the self._dcelem_type += type
resource. return self._dcelem_type
For more information see:
http://dublincore.org/documents/dcmi-terms/#elements-type
:param type: Type or list of types.
:param replace: Replace alredy set types (deault: False).
:returns: List of types.
'''
if not type is None:
if not isinstance(type, list):
type = [type]
if replace or not self._dcelem_type:
self._dcelem_type = []
self._dcelem_type += type
return self._dcelem_type
class DcExtension(DcBaseExtension): class DcExtension(DcBaseExtension):
'''Dublin Core Elements extension for podcasts. '''Dublin Core Elements extension for podcasts.
''' '''
class DcEntryExtension(DcBaseExtension): class DcEntryExtension(DcBaseExtension):
'''Dublin Core Elements extension for podcasts. '''Dublin Core Elements extension for podcasts.
''' '''
def extend_atom(self, entry): def extend_atom(self, entry):
'''Add dc elements to an atom item. Alters the item itself. '''Add dc elements to an atom item. Alters the item itself.
:param entry: An atom entry element. :param entry: An atom entry element.
:returns: The entry element. :returns: The entry element.
''' '''
self._extend_xml(entry) self._extend_xml(entry)
return entry return entry
def extend_rss(self, item): def extend_rss(self, item):
'''Add dc elements to a RSS item. Alters the item itself. '''Add dc elements to a RSS item. Alters the item itself.
:param item: A RSS item element. :param item: A RSS item element.
:returns: The item element. :returns: The item element.
''' '''
self._extend_xml(item) self._extend_xml(item)
return item return item

View file

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
feedgen.ext.podcast feedgen.ext.podcast
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to produce podcasts. Extends the FeedGenerator to produce podcasts.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
from lxml import etree from lxml import etree
@ -17,343 +17,343 @@ from feedgen.compat import string_types
class PodcastExtension(BaseExtension): class PodcastExtension(BaseExtension):
'''FeedGenerator extension for podcasts. '''FeedGenerator extension for podcasts.
''' '''
def __init__(self):
def __init__(self): # ITunes tags
## ITunes tags # http://www.apple.com/itunes/podcasts/specs.html#rss
# http://www.apple.com/itunes/podcasts/specs.html#rss self.__itunes_author = None
self.__itunes_author = None self.__itunes_block = None
self.__itunes_block = None self.__itunes_category = None
self.__itunes_category = None self.__itunes_image = None
self.__itunes_image = None self.__itunes_explicit = None
self.__itunes_explicit = None self.__itunes_complete = None
self.__itunes_complete = None self.__itunes_new_feed_url = None
self.__itunes_new_feed_url = None self.__itunes_owner = None
self.__itunes_owner = None self.__itunes_subtitle = None
self.__itunes_subtitle = None self.__itunes_summary = None
self.__itunes_summary = None
def extend_ns(self):
return {'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd'}
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.
def extend_rss(self, rss_feed): :returns: The feed root element.
'''Extend an RSS feed root with set itunes fields. '''
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
:returns: The feed root element. channel = rss_feed[0]
'''
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' if self.__itunes_author:
channel = rss_feed[0] author = etree.SubElement(channel, '{%s}author' % ITUNES_NS)
author.text = self.__itunes_author
if self.__itunes_author:
author = etree.SubElement(channel, '{%s}author' % ITUNES_NS) if self.__itunes_block is not None:
author.text = self.__itunes_author block = etree.SubElement(channel, '{%s}block' % ITUNES_NS)
block.text = 'yes' if self.__itunes_block else 'no'
if not self.__itunes_block is None:
block = etree.SubElement(channel, '{%s}block' % ITUNES_NS) for c in self.__itunes_category or []:
block.text = 'yes' if self.__itunes_block else 'no' if not c.get('cat'):
continue
for c in self.__itunes_category or []: category = channel.find(
if not c.get('cat'): '{%s}category[@text="%s"]' % (ITUNES_NS, c.get('cat')))
continue if category is None:
category = channel.find('{%s}category[@text="%s"]' % (ITUNES_NS,c.get('cat'))) category = etree.SubElement(channel,
if category == None: '{%s}category' % ITUNES_NS)
category = etree.SubElement(channel, '{%s}category' % ITUNES_NS) category.attrib['text'] = c.get('cat')
category.attrib['text'] = c.get('cat')
if c.get('sub'):
if c.get('sub'): subcategory = etree.SubElement(category,
subcategory = etree.SubElement(category, '{%s}category' % ITUNES_NS) '{%s}category' % ITUNES_NS)
subcategory.attrib['text'] = c.get('sub') subcategory.attrib['text'] = c.get('sub')
if self.__itunes_image: if self.__itunes_image:
image = etree.SubElement(channel, '{%s}image' % ITUNES_NS) image = etree.SubElement(channel, '{%s}image' % ITUNES_NS)
image.attrib['href'] = self.__itunes_image image.attrib['href'] = self.__itunes_image
if self.__itunes_explicit in ('yes', 'no', 'clean'): if self.__itunes_explicit in ('yes', 'no', 'clean'):
explicit = etree.SubElement(channel, '{%s}explicit' % ITUNES_NS) explicit = etree.SubElement(channel, '{%s}explicit' % ITUNES_NS)
explicit.text = self.__itunes_explicit explicit.text = self.__itunes_explicit
if self.__itunes_complete in ('yes', 'no'): if self.__itunes_complete in ('yes', 'no'):
complete = etree.SubElement(channel, '{%s}complete' % ITUNES_NS) complete = etree.SubElement(channel, '{%s}complete' % ITUNES_NS)
complete.text = self.__itunes_complete complete.text = self.__itunes_complete
if self.__itunes_new_feed_url: if self.__itunes_new_feed_url:
new_feed_url = etree.SubElement(channel, '{%s}new-feed-url' % ITUNES_NS) new_feed_url = etree.SubElement(channel,
new_feed_url.text = self.__itunes_new_feed_url '{%s}new-feed-url' % ITUNES_NS)
new_feed_url.text = self.__itunes_new_feed_url
if self.__itunes_owner:
owner = etree.SubElement(channel, '{%s}owner' % ITUNES_NS) if self.__itunes_owner:
owner_name = etree.SubElement(owner, '{%s}name' % ITUNES_NS) owner = etree.SubElement(channel, '{%s}owner' % ITUNES_NS)
owner_name.text = self.__itunes_owner.get('name') owner_name = etree.SubElement(owner, '{%s}name' % ITUNES_NS)
owner_email = etree.SubElement(owner, '{%s}email' % ITUNES_NS) owner_name.text = self.__itunes_owner.get('name')
owner_email.text = self.__itunes_owner.get('email') owner_email = etree.SubElement(owner, '{%s}email' % ITUNES_NS)
owner_email.text = self.__itunes_owner.get('email')
if self.__itunes_subtitle:
subtitle = etree.SubElement(channel, '{%s}subtitle' % ITUNES_NS) if self.__itunes_subtitle:
subtitle.text = self.__itunes_subtitle subtitle = etree.SubElement(channel, '{%s}subtitle' % ITUNES_NS)
subtitle.text = self.__itunes_subtitle
if self.__itunes_summary:
summary = etree.SubElement(channel, '{%s}summary' % ITUNES_NS) if self.__itunes_summary:
summary.text = self.__itunes_summary summary = etree.SubElement(channel, '{%s}summary' % ITUNES_NS)
summary.text = self.__itunes_summary
return rss_feed
return rss_feed
def itunes_author(self, itunes_author=None): def itunes_author(self, itunes_author=None):
'''Get or set the itunes:author. The content of this tag is shown in the '''Get or set the itunes:author. The content of this tag is shown in
Artist column in iTunes. If the tag is not present, iTunes uses the the Artist column in iTunes. If the tag is not present, iTunes uses the
contents of the <author> tag. If <itunes:author> is not present at the contents of the <author> tag. If <itunes:author> is not present at the
feed level, iTunes will use the contents of <managingEditor>. feed level, iTunes will use the contents of <managingEditor>.
:param itunes_author: The author of the podcast. :param itunes_author: The author of the podcast.
:returns: The author of the podcast. :returns: The author of the podcast.
''' '''
if not itunes_author is None: if itunes_author is not None:
self.__itunes_author = itunes_author self.__itunes_author = itunes_author
return self.__itunes_author return self.__itunes_author
def itunes_block(self, itunes_block=None):
def itunes_block(self, itunes_block=None): '''Get or set the ITunes block attribute. Use this to prevent the
'''Get or set the ITunes block attribute. Use this to prevent the entire entire podcast from appearing in the iTunes podcast directory.
podcast from appearing in the iTunes podcast directory.
:param itunes_block: Block the podcast.
:param itunes_block: Block the podcast. :returns: If the podcast is blocked.
:returns: If the podcast is blocked. '''
''' if itunes_block is not None:
if not itunes_block is None: self.__itunes_block = itunes_block
self.__itunes_block = itunes_block return self.__itunes_block
return self.__itunes_block
def itunes_category(self, itunes_category=None, replace=False, **kwargs):
def itunes_category(self, itunes_category=None, replace=False, **kwargs): '''Get or set the ITunes category which appears in the category column
'''Get or set the ITunes category which appears in the category column and in iTunes Store Browser.
and in iTunes Store Browser.
The (sub-)category has to be one from the values defined at
The (sub-)category has to be one from the values defined at http://www.apple.com/itunes/podcasts/specs.html#categories
http://www.apple.com/itunes/podcasts/specs.html#categories
This method can be called with:
This method can be called with:
- the fields of an itunes_category as keyword arguments
- the fields of an itunes_category as keyword arguments - the fields of an itunes_category as a dictionary
- the fields of an itunes_category as a dictionary - a list of dictionaries containing the itunes_category fields
- a list of dictionaries containing the itunes_category fields
An itunes_category has the following fields:
An itunes_category has the following fields:
- *cat* name for a category.
- *cat* name for a category. - *sub* name for a subcategory, child of category
- *sub* name for a subcategory, child of category
If a podcast has more than one subcategory from the same category, the
If a podcast has more than one subcategory from the same category, the category is called more than once.
category is called more than once.
Likei the parameter::
Likei the parameter::
[{"cat":"Arts","sub":"Design"},{"cat":"Arts","sub":"Food"}]
[{"cat":"Arts","sub":"Design"},{"cat":"Arts","sub":"Food"}]
would become::
would become::
<itunes:category text="Arts">
<itunes:category text="Arts"> <itunes:category text="Design"/>
<itunes:category text="Design"/> <itunes:category text="Food"/>
<itunes:category text="Food"/> </itunes:category>
</itunes:category>
:param itunes_category: Dictionary or list of dictionaries with
:param itunes_category: Dictionary or list of dictionaries with itunes_category data.
itunes_category data. :param replace: Add or replace old data.
:param replace: Add or replace old data. :returns: List of itunes_categories as dictionaries.
:returns: List of itunes_categories as dictionaries.
---
---
**Important note about deprecated parameter syntax:** Old version of
**Important note about deprecated parameter syntax:** Old version of the the feedgen did only support one category plus one subcategory which
feedgen did only support one category plus one subcategory which would be would be passed to this ducntion as first two parameters. For
passed to this ducntion as first two parameters. For compatibility compatibility reasons, this still works but should not be used any may
reasons, this still works but should not be used any may be removed at be removed at any time.
any time. '''
''' # Ensure old API still works for now. Note that the API is deprecated
# Ensure old API still works for now. Note that the API is deprecated and # and this fallback may be removed at any time.
# this fallback may be removed at any time. if isinstance(itunes_category, string_types):
if isinstance(itunes_category, string_types): itunes_category = {'cat': itunes_category}
itunes_category = {'cat':itunes_category} if replace:
if replace: itunes_category['sub'] = replace
itunes_category['sub'] = replace replace = True
replace=True if itunes_category is None and kwargs:
if itunes_category is None and kwargs: itunes_category = kwargs
itunes_category = kwargs if itunes_category is not None:
if not itunes_category is None: if replace or self.__itunes_category is None:
if replace or self.__itunes_category is None: self.__itunes_category = []
self.__itunes_category = [] self.__itunes_category += ensure_format(itunes_category,
self.__itunes_category += ensure_format( itunes_category, set(['cat', 'sub']),
set(['cat', 'sub']), set(['cat'])) set(['cat']))
return self.__itunes_category return self.__itunes_category
def itunes_image(self, itunes_image=None):
def itunes_image(self, itunes_image=None): '''Get or set the image for the podcast. This tag specifies the artwork
'''Get or set the image for the podcast. This tag specifies the artwork for your podcast. Put the URL to the image in the href attribute.
for your podcast. Put the URL to the image in the href attribute. iTunes iTunes prefers square .jpg images that are at least 1400x1400 pixels,
prefers square .jpg images that are at least 1400x1400 pixels, which is which is different from what is specified for the standard RSS image
different from what is specified for the standard RSS image tag. In order tag. In order for a podcast to be eligible for an iTunes Store feature,
for a podcast to be eligible for an iTunes Store feature, the the accompanying image must be at least 1400x1400 pixels.
accompanying image must be at least 1400x1400 pixels.
iTunes supports images in JPEG and PNG formats with an RGB color space
iTunes supports images in JPEG and PNG formats with an RGB color space (CMYK is not supported). The URL must end in ".jpg" or ".png". If the
(CMYK is not supported). The URL must end in ".jpg" or ".png". If the <itunes:image> tag is not present, iTunes will use the contents of the
<itunes:image> tag is not present, iTunes will use the contents of the RSS image tag.
RSS image tag.
If you change your podcasts image, also change the files name. iTunes
If you change your podcasts image, also change the files name. iTunes may not change the image if it checks your feed and the image URL is
may not change the image if it checks your feed and the image URL is the the same. The server hosting your cover art image must allow HTTP head
same. The server hosting your cover art image must allow HTTP head requests for iTS to be able to automatically update your cover art.
requests for iTS to be able to automatically update your cover art.
:param itunes_image: Image of the podcast.
:param itunes_image: Image of the podcast. :returns: Image of the podcast.
:returns: Image of the podcast. '''
''' if itunes_image is not None:
if not itunes_image is None: if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
if not ( itunes_image.endswith('.jpg') or itunes_image.endswith('.png') ): self.__itunes_image = itunes_image
ValueError('Image file must be png or jpg') else:
self.__itunes_image = itunes_image ValueError('Image file must be png or jpg')
return self.__itunes_image return self.__itunes_image
def itunes_explicit(self, itunes_explicit=None):
def itunes_explicit(self, itunes_explicit=None): '''Get or the the itunes:explicit value of the podcast. This tag should
'''Get or the the itunes:explicit value of the podcast. This tag should be used to indicate whether your podcast contains explicit material.
be used to indicate whether your podcast contains explicit material. The The three values for this tag are "yes", "no", and "clean".
three values for this tag are "yes", "no", and "clean".
If you populate this tag with "yes", an "explicit" parental advisory
If you populate this tag with "yes", an "explicit" parental advisory graphic will appear next to your podcast artwork on the iTunes Store
graphic will appear next to your podcast artwork on the iTunes Store and and in the Name column in iTunes. If the value is "clean", the parental
in the Name column in iTunes. If the value is "clean", the parental advisory type is considered Clean, meaning that no explicit language or
advisory type is considered Clean, meaning that no explicit language or adult content is included anywhere in the episodes, and a "clean"
adult content is included anywhere in the episodes, and a "clean" graphic graphic will appear. If the explicit tag is present and has any other
will appear. If the explicit tag is present and has any other value value (e.g., "no"), you see no indicator blank is the default
(e.g., "no"), you see no indicator blank is the default advisory type. advisory type.
:param itunes_explicit: If the podcast contains explicit material. :param itunes_explicit: If the podcast contains explicit material.
:returns: If the podcast contains explicit material. :returns: If the podcast contains explicit material.
''' '''
if not itunes_explicit is None: if itunes_explicit is not None:
if not itunes_explicit in ('', 'yes', 'no', 'clean'): if itunes_explicit not in ('', 'yes', 'no', 'clean'):
raise ValueError('Invalid value for explicit tag') raise ValueError('Invalid value for explicit tag')
self.__itunes_explicit = itunes_explicit self.__itunes_explicit = itunes_explicit
return self.__itunes_explicit return self.__itunes_explicit
def itunes_complete(self, itunes_complete=None):
def itunes_complete(self, itunes_complete=None): '''Get or set the itunes:complete value of the podcast. This tag can be
'''Get or set the itunes:complete value of the podcast. This tag can be used to indicate the completion of a podcast.
used to indicate the completion of a podcast.
If you populate this tag with "yes", you are indicating that no more
If you populate this tag with "yes", you are indicating that no more episodes will be added to the podcast. If the <itunes:complete> tag is
episodes will be added to the podcast. If the <itunes:complete> tag is present and has any other value (e.g. no), it will have no effect on
present and has any other value (e.g. no), it will have no effect on the podcast.
the podcast.
:param itunes_complete: If the podcast is complete.
:param itunes_complete: If the podcast is complete. :returns: If the podcast is complete.
:returns: If the podcast is complete. '''
''' if itunes_complete is not None:
if not itunes_complete is None: if itunes_complete not in ('yes', 'no', '', True, False):
if not itunes_complete in ('yes', 'no', '', True, False): raise ValueError('Invalid value for complete tag')
raise ValueError('Invalid value for complete tag') if itunes_complete is True:
if itunes_complete == True: itunes_complete = 'yes'
itunes_complete = 'yes' if itunes_complete is False:
if itunes_complete == False: itunes_complete = 'no'
itunes_complete = 'no' self.__itunes_complete = itunes_complete
self.__itunes_complete = itunes_complete return self.__itunes_complete
return self.__itunes_complete
def itunes_new_feed_url(self, itunes_new_feed_url=None):
'''Get or set the new-feed-url property of the podcast. This tag allows
def itunes_new_feed_url(self, itunes_new_feed_url=None): you to change the URL where the podcast feed is located
'''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
After adding the tag to your old feed, you should maintain the old feed 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.
:param itunes_new_feed_url: New feed URL. '''
:returns: New feed URL. if itunes_new_feed_url is not None:
''' self.__itunes_new_feed_url = itunes_new_feed_url
if not itunes_new_feed_url is None: return self.__itunes_new_feed_url
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
def itunes_owner(self, name=None, email=None): communication specifically about the podcast. It will not be publicly
'''Get or set the itunes:owner of the podcast. This tag contains displayed.
information that will be used to contact the owner of the podcast for
communication specifically about the podcast. It will not be publicly :param itunes_owner: The owner of the feed.
displayed. :returns: Data of the owner of the feed.
'''
:param itunes_owner: The owner of the feed. if name is not None:
:returns: Data of the owner of the feed. if name and email:
''' self.__itunes_owner = {'name': name, 'email': email}
if not name is None: elif not name and not email:
if name and email: self.__itunes_owner = None
self.__itunes_owner = {'name':name, 'email':email} else:
elif not name and not email: raise ValueError('Both name and email have to be set.')
self.__itunes_owner = None return self.__itunes_owner
else:
raise ValueError('Both name and email have to be set.') def itunes_subtitle(self, itunes_subtitle=None):
return self.__itunes_owner '''Get or set the itunes:subtitle value for the podcast. The contents of
this tag are shown in the Description column in iTunes. The subtitle
displays best if it is only a few words long.
def itunes_subtitle(self, itunes_subtitle=None):
'''Get or set the itunes:subtitle value for the podcast. The contents of :param itunes_subtitle: Subtitle of the podcast.
this tag are shown in the Description column in iTunes. The subtitle :returns: Subtitle of the podcast.
displays best if it is only a few words long. '''
if itunes_subtitle is not None:
:param itunes_subtitle: Subtitle of the podcast. self.__itunes_subtitle = itunes_subtitle
:returns: Subtitle of the podcast. return self.__itunes_subtitle
'''
if not itunes_subtitle is None: def itunes_summary(self, itunes_summary=None):
self.__itunes_subtitle = itunes_subtitle '''Get or set the itunes:summary value for the podcast. The contents of
return self.__itunes_subtitle 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
def itunes_summary(self, itunes_summary=None): `<itunes:summary>` is not included, the contents of the <description>
'''Get or set the itunes:summary value for the podcast. The contents of 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 :param itunes_summary: Summary of the podcast.
for your podcast. This field can be up to 4000 characters. If :returns: Summary of the podcast.
<itunes:summary> is not included, the contents of the <description> tag '''
are used. if itunes_summary is not None:
self.__itunes_summary = itunes_summary
:param itunes_summary: Summary of the podcast. return self.__itunes_summary
:returns: Summary of the podcast.
''' _itunes_categories = {
if not itunes_summary is None: 'Arts': [
self.__itunes_summary = itunes_summary 'Design', 'Fashion & Beauty', 'Food', 'Literature',
return self.__itunes_summary 'Performing Arts', 'Visual Arts'],
'Business': [
'Business News', 'Careers', 'Investing',
_itunes_categories = { 'Management & Marketing', 'Shopping'],
'Arts': [ 'Design', 'Fashion & Beauty', 'Food', 'Literature', 'Comedy': [],
'Performing Arts', 'Visual Arts' ], 'Education': [
'Business' : [ 'Business News', 'Careers', 'Investing', 'Education', 'Education Technology', 'Higher Education',
'Management & Marketing', 'Shopping' ], 'K-12', 'Language Courses', 'Training'],
'Comedy' : [], 'Games & Hobbies': [
'Education' : [ 'Education', 'Education Technology', 'Automotive', 'Aviation', 'Hobbies', 'Other Games',
'Higher Education', 'K-12', 'Language Courses', 'Training' ], 'Video Games'],
'Games & Hobbies' : [ 'Automotive', 'Aviation', 'Hobbies', 'Government & Organizations': [
'Other Games', 'Video Games' ], 'Local', 'National', 'Non-Profit', 'Regional'],
'Government & Organizations' : [ 'Local', 'National', 'Non-Profit', 'Health': [
'Regional' ], 'Alternative Health', 'Fitness & Nutrition', 'Self-Help',
'Health' : [ 'Alternative Health', 'Fitness & Nutrition', 'Self-Help', 'Sexuality'],
'Sexuality' ], 'Kids & Family': [],
'Kids & Family' : [], 'Music': [],
'Music' : [], 'News & Politics': [],
'News & Politics' : [], 'Religion & Spirituality': [
'Religion & Spirituality' : [ 'Buddhism', 'Christianity', 'Hinduism', 'Buddhism', 'Christianity', 'Hinduism', 'Islam', 'Judaism',
'Islam', 'Judaism', 'Other', 'Spirituality' ], 'Other', 'Spirituality'],
'Science & Medicine' : [ 'Medicine', 'Natural Sciences', 'Science & Medicine': [
'Social Sciences' ], 'Medicine', 'Natural Sciences', 'Social Sciences'],
'Society & Culture' : [ 'History', 'Personal Journals', 'Philosophy', 'Society & Culture': [
'Places & Travel' ], 'History', 'Personal Journals', 'Philosophy',
'Sports & Recreation' : [ 'Amateur', 'College & High School', 'Places & Travel'],
'Outdoor', 'Professional' ], 'Sports & Recreation': [
'Technology' : [ 'Gadgets', 'Tech News', 'Podcasting', 'Amateur', 'College & High School', 'Outdoor', 'Professional'],
'Software How-To' ], 'Technology': [
'TV & Film' : [] 'Gadgets', 'Tech News', 'Podcasting', 'Software How-To'],
} 'TV & Film': []}

View file

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
feedgen.ext.podcast_entry feedgen.ext.podcast_entry
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Extends the feedgen to produce podcasts. Extends the feedgen to produce podcasts.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013-2016, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
from lxml import etree from lxml import etree
@ -15,229 +15,230 @@ from feedgen.ext.base import BaseEntryExtension
class PodcastEntryExtension(BaseEntryExtension): class PodcastEntryExtension(BaseEntryExtension):
'''FeedEntry extension for podcasts. '''FeedEntry extension for podcasts.
''' '''
def __init__(self):
# ITunes tags
# http://www.apple.com/itunes/podcasts/specs.html#rss
self.__itunes_author = None
self.__itunes_block = None
self.__itunes_image = None
self.__itunes_duration = None
self.__itunes_explicit = None
self.__itunes_is_closed_captioned = None
self.__itunes_order = None
self.__itunes_subtitle = None
self.__itunes_summary = None
def __init__(self): def extend_rss(self, entry):
## ITunes tags '''Add additional fields to an RSS item.
# http://www.apple.com/itunes/podcasts/specs.html#rss
self.__itunes_author = None
self.__itunes_block = None
self.__itunes_image = None
self.__itunes_duration = None
self.__itunes_explicit = None
self.__itunes_is_closed_captioned = None
self.__itunes_order = None
self.__itunes_subtitle = None
self.__itunes_summary = None
:param feed: The RSS item XML element to use.
'''
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd'
def extend_rss(self, entry): if self.__itunes_author:
'''Add additional fields to an RSS item. author = etree.SubElement(entry, '{%s}author' % ITUNES_NS)
author.text = self.__itunes_author
:param feed: The RSS item XML element to use. if self.__itunes_block is not None:
''' block = etree.SubElement(entry, '{%s}block' % ITUNES_NS)
ITUNES_NS = 'http://www.itunes.com/dtds/podcast-1.0.dtd' block.text = 'yes' if self.__itunes_block else 'no'
if self.__itunes_author: if self.__itunes_image:
author = etree.SubElement(entry, '{%s}author' % ITUNES_NS) image = etree.SubElement(entry, '{%s}image' % ITUNES_NS)
author.text = self.__itunes_author image.attrib['href'] = self.__itunes_image
if not self.__itunes_block is None: if self.__itunes_duration:
block = etree.SubElement(entry, '{%s}block' % ITUNES_NS) duration = etree.SubElement(entry, '{%s}duration' % ITUNES_NS)
block.text = 'yes' if self.__itunes_block else 'no' duration.text = self.__itunes_duration
if self.__itunes_image: if self.__itunes_explicit in ('yes', 'no', 'clean'):
image = etree.SubElement(entry, '{%s}image' % ITUNES_NS) explicit = etree.SubElement(entry, '{%s}explicit' % ITUNES_NS)
image.attrib['href'] = self.__itunes_image explicit.text = self.__itunes_explicit
if self.__itunes_duration: if self.__itunes_is_closed_captioned is not None:
duration = etree.SubElement(entry, '{%s}duration' % ITUNES_NS) is_closed_captioned = etree.SubElement(
duration.text = self.__itunes_duration entry, '{%s}isClosedCaptioned' % ITUNES_NS)
if self.__itunes_is_closed_captioned:
is_closed_captioned.text = 'yes'
else:
is_closed_captioned.text = 'no'
if self.__itunes_explicit in ('yes', 'no', 'clean'): if self.__itunes_order is not None and self.__itunes_order >= 0:
explicit = etree.SubElement(entry, '{%s}explicit' % ITUNES_NS) order = etree.SubElement(entry, '{%s}order' % ITUNES_NS)
explicit.text = self.__itunes_explicit order.text = str(self.__itunes_order)
if not self.__itunes_is_closed_captioned is None: if self.__itunes_subtitle:
is_closed_captioned = etree.SubElement(entry, '{%s}isClosedCaptioned' % ITUNES_NS) subtitle = etree.SubElement(entry, '{%s}subtitle' % ITUNES_NS)
is_closed_captioned.text = 'yes' if self.__itunes_is_closed_captioned else 'no' subtitle.text = self.__itunes_subtitle
if not self.__itunes_order is None and self.__itunes_order >= 0: if self.__itunes_summary:
order = etree.SubElement(entry, '{%s}order' % ITUNES_NS) summary = etree.SubElement(entry, '{%s}summary' % ITUNES_NS)
order.text = str(self.__itunes_order) summary.text = self.__itunes_summary
return entry
if self.__itunes_subtitle: def itunes_author(self, itunes_author=None):
subtitle = etree.SubElement(entry, '{%s}subtitle' % ITUNES_NS) '''Get or set the itunes:author of the podcast episode. The content of
subtitle.text = self.__itunes_subtitle 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>.
if self.__itunes_summary: :param itunes_author: The author of the podcast.
summary = etree.SubElement(entry, '{%s}summary' % ITUNES_NS) :returns: The author of the podcast.
summary.text = self.__itunes_summary '''
return entry 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.
def itunes_author(self, itunes_author=None): :param itunes_block: Block podcast episodes.
'''Get or set the itunes:author of the podcast episode. The content of :returns: If the podcast episode is blocked.
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> if itunes_block is not None:
is not present at the feed level, iTunes will use the contents of self.__itunes_block = itunes_block
<managingEditor>. return self.__itunes_block
:param itunes_author: The author of the podcast. def itunes_image(self, itunes_image=None):
:returns: The author of the podcast. '''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
if not itunes_author is None: attribute. iTunes prefers square .jpg images that are at least
self.__itunes_author = itunes_author 1400x1400 pixels, which is different from what is specified for the
return self.__itunes_author 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
<itunes:image> tag is not present, iTunes will use the contents of the
RSS image tag.
def itunes_block(self, itunes_block=None): If you change your podcasts image, also change the files name. iTunes
'''Get or set the ITunes block attribute. Use this to prevent episodes may not change the image if it checks your feed and the image URL is
from appearing in the iTunes podcast directory. 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_block: Block podcast episodes. :param itunes_image: Image of the podcast.
:returns: If the podcast episode is blocked. :returns: Image of the podcast.
''' '''
if not itunes_block is None: if itunes_image is not None:
self.__itunes_block = itunes_block if itunes_image.endswith('.jpg') or itunes_image.endswith('.png'):
return self.__itunes_block 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.
def itunes_image(self, itunes_image=None): The tag can be formatted HH:MM:SS, H:MM:SS, MM:SS, or M:SS (H = hours,
'''Get or set the image for the podcast episode. This tag specifies the M = minutes, S = seconds). If an integer is provided (no colon
artwork for your podcast. Put the URL to the image in the href attribute. present), the value is assumed to be in seconds. If one colon is
iTunes prefers square .jpg images that are at least 1400x1400 pixels, present, the number to the left is assumed to be minutes, and the
which is different from what is specified for the standard RSS image tag. number to the right is assumed to be seconds. If more than two colons
In order for a podcast to be eligible for an iTunes Store feature, the are present, the numbers farthest to the right are ignored.
accompanying image must be at least 1400x1400 pixels.
iTunes supports images in JPEG and PNG formats with an RGB color space :param itunes_duration: Duration of the podcast episode.
(CMYK is not supported). The URL must end in ".jpg" or ".png". If the :returns: Duration of the podcast episode.
<itunes:image> tag is not present, iTunes will use the contents of the '''
RSS image tag. if itunes_duration is not None:
itunes_duration = str(itunes_duration)
if len(itunes_duration.split(':')) > 3 or \
itunes_duration.lstrip('0123456789:') != '':
ValueError('Invalid duration format')
self.__itunes_duration = itunes_duration
return self.itunes_duration
If you change your podcasts image, also change the files name. iTunes def itunes_explicit(self, itunes_explicit=None):
may not change the image if it checks your feed and the image URL is the '''Get or the the itunes:explicit value of the podcast episode. This
same. The server hosting your cover art image must allow HTTP head tag should be used to indicate whether your podcast episode contains
requests for iTS to be able to automatically update your cover art. explicit material. The three values for this tag are "yes", "no", and
"clean".
:param itunes_image: Image of the podcast. If you populate this tag with "yes", an "explicit" parental advisory
:returns: Image of the podcast. 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
if not itunes_image is None: advisory type is considered Clean, meaning that no explicit language or
if not ( itunes_image.endswith('.jpg') or itunes_image.endswith('.png') ): adult content is included anywhere in the episodes, and a "clean"
ValueError('Image file must be png or jpg') graphic will appear. If the explicit tag is present and has any other
self.__itunes_image = itunes_image value (e.g., "no"), you see no indicator blank is the default
return self.__itunes_image advisory type.
:param itunes_explicit: If the podcast episode contains explicit
material.
:returns: If the podcast episode contains explicit material.
'''
if itunes_explicit is not None:
if itunes_explicit not in ('', 'yes', 'no', 'clean'):
raise ValueError('Invalid value for explicit tag')
self.__itunes_explicit = itunes_explicit
return self.__itunes_explicit
def itunes_duration(self, itunes_duration=None): def itunes_is_closed_captioned(self, itunes_is_closed_captioned=None):
'''Get or set the duration of the podcast episode. The content of this '''Get or set the is_closed_captioned value of the podcast episode.
tag is shown in the Time column in iTunes. 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”.
The tag can be formatted HH:MM:SS, H:MM:SS, MM:SS, or M:SS (H = hours, :param is_closed_captioned: If the episode has closed captioning
M = minutes, S = seconds). If an integer is provided (no colon present), support.
the value is assumed to be in seconds. If one colon is present, the :returns: If the episode has closed captioning support.
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 if itunes_is_closed_captioned is not None:
numbers farthest to the right are ignored. self.__itunes_is_closed_captioned = \
itunes_is_closed_captioned in ('yes', True)
return self.__itunes_is_closed_captioned
:param itunes_duration: Duration of the podcast episode. def itunes_order(self, itunes_order=None):
:returns: Duration of the podcast episode. '''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.
if not itunes_duration is None:
itunes_duration = str(itunes_duration)
if len(itunes_duration.split(':')) > 3 or \
itunes_duration.lstrip('0123456789:') != '':
ValueError('Invalid duration format')
self.__itunes_duration = itunes_duration
return self.itunes_duration
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).
def itunes_explicit(self, itunes_explicit=None): To remove the order from the episode set the order to a value below
'''Get or the the itunes:explicit value of the podcast episode. This tag zero.
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 :param itunes_order: The order of the episode.
graphic will appear next to your podcast artwork on the iTunes Store and :returns: The order of the episode.
in the Name column in iTunes. If the value is "clean", the parental '''
advisory type is considered Clean, meaning that no explicit language or if itunes_order is not None:
adult content is included anywhere in the episodes, and a "clean" graphic self.__itunes_order = int(itunes_order)
will appear. If the explicit tag is present and has any other value return self.__itunes_order
(e.g., "no"), you see no indicator blank is the default advisory type.
:param itunes_explicit: If the podcast episode contains explicit material. def itunes_subtitle(self, itunes_subtitle=None):
:returns: If the podcast episode contains explicit material. '''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
if not itunes_explicit is None: subtitle displays best if it is only a few words long.
if not itunes_explicit in ('', 'yes', 'no', 'clean'):
raise ValueError('Invalid value for explicit tag')
self.__itunes_explicit = itunes_explicit
return self.__itunes_explicit
:param itunes_subtitle: Subtitle of the podcast episode.
:returns: Subtitle of the podcast episode.
'''
if itunes_subtitle is not None:
self.__itunes_subtitle = itunes_subtitle
return self.__itunes_subtitle
def itunes_is_closed_captioned(self, itunes_is_closed_captioned=None): def itunes_summary(self, itunes_summary=None):
'''Get or set the is_closed_captioned value of the podcast episode. This '''Get or set the itunes:summary value for the podcast episode. The
tag should be used if your podcast includes a video episode with embedded contents of this tag are shown in a separate window that appears when
closed captioning support. The two values for this tag are "yes" and the "circled i" in the Description column is clicked. It also appears
"no”. 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 is_closed_captioned: If the episode has closed captioning support. :param itunes_summary: Summary of the podcast episode.
:returns: If the episode has closed captioning support. :returns: Summary of the podcast episode.
''' '''
if not itunes_is_closed_captioned is None: if itunes_summary is not None:
self.__itunes_is_closed_captioned = itunes_is_closed_captioned in ('yes', True) self.__itunes_summary = itunes_summary
return self.__itunes_is_closed_captioned return self.__itunes_summary
def itunes_order(self, itunes_order=None):
'''Get or set the itunes:order value of the podcast episode. This tag can
be used to override the default ordering of episodes on the store.
This tag is used at an <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).
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:
self.__itunes_order = int(itunes_order)
return self.__itunes_order
def itunes_subtitle(self, itunes_subtitle=None):
'''Get or set the itunes:subtitle value for the podcast episode. The
contents of this tag are shown in the Description column in iTunes. The
subtitle displays best if it is only a few words long.
:param itunes_subtitle: Subtitle of the podcast episode.
:returns: Subtitle of the podcast episode.
'''
if not itunes_subtitle is 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.
:param itunes_summary: Summary of the podcast episode.
:returns: Summary of the podcast episode.
'''
if not itunes_summary is None:
self.__itunes_summary = itunes_summary
return self.__itunes_summary

View file

@ -1,127 +1,126 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
feedgen.ext.torrent feedgen.ext.torrent
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Extends the FeedGenerator to produce torrent feeds. Extends the FeedGenerator to produce torrent feeds.
:copyright: 2016, Raspbeguy <raspbeguy@hashtagueule.fr> :copyright: 2016, Raspbeguy <raspbeguy@hashtagueule.fr>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
from lxml import etree from lxml import etree
from feedgen.ext.base import BaseExtension,BaseEntryExtension from feedgen.ext.base import BaseExtension, BaseEntryExtension
TORRENT_NS = 'http://xmlns.ezrss.it/0.1/dtd/' TORRENT_NS = 'http://xmlns.ezrss.it/0.1/dtd/'
class TorrentExtension(BaseExtension): class TorrentExtension(BaseExtension):
'''FeedGenerator extension for torrent feeds. '''FeedGenerator extension for torrent feeds.
''' '''
def extend_ns(self): def extend_ns(self):
return {'torrent' : TORRENT_NS} return {'torrent': TORRENT_NS}
class TorrentEntryExtension(BaseEntryExtension): class TorrentEntryExtension(BaseEntryExtension):
'''FeedEntry extention for torrent feeds '''FeedEntry extention for torrent feeds
''' '''
def __init__(self): def __init__(self):
self.__torrent_filename = None self.__torrent_filename = None
self.__torrent_infohash = None self.__torrent_infohash = None
self.__torrent_contentlength = None self.__torrent_contentlength = None
self.__torrent_seeds = None self.__torrent_seeds = None
self.__torrent_peers = None self.__torrent_peers = None
self.__torrent=verified = None
def extend_rss(self, entry): def extend_rss(self, entry):
'''Add additional fields to an RSS item. '''Add additional fields to an RSS item.
:param feed: The RSS item XML element to use. :param feed: The RSS item XML element to use.
''' '''
if self.__torrent_filename: if self.__torrent_filename:
filename = etree.SubElement(entry, '{%s}filename' % TORRENT_NS) filename = etree.SubElement(entry, '{%s}filename' % TORRENT_NS)
filename.text = self.__torrent_filename filename.text = self.__torrent_filename
if self.__torrent_contentlength: if self.__torrent_contentlength:
contentlength = etree.SubElement(entry, '{%s}contentlength' % TORRENT_NS) contentlength = etree.SubElement(entry,
contentlength.text = self.__torrent_contentlength '{%s}contentlength' % TORRENT_NS)
contentlength.text = self.__torrent_contentlength
if self.__torrent_infohash: if self.__torrent_infohash:
infohash = etree.SubElement(entry, '{%s}infohash' % TORRENT_NS) infohash = etree.SubElement(entry, '{%s}infohash' % TORRENT_NS)
infohash.text = self.__torrent_infohash infohash.text = self.__torrent_infohash
magnet = etree.SubElement(entry, '{%s}magneturi' % TORRENT_NS) magnet = etree.SubElement(entry, '{%s}magneturi' % TORRENT_NS)
magnet.text = 'magnet:?xt=urn:btih:' + self.__torrent_infohash magnet.text = 'magnet:?xt=urn:btih:' + self.__torrent_infohash
if self.__torrent_seeds: if self.__torrent_seeds:
seeds = etree.SubElement(entry, '{%s}seed' % TORRENT_NS) seeds = etree.SubElement(entry, '{%s}seed' % TORRENT_NS)
seeds.text = self.__torrent_seeds seeds.text = self.__torrent_seeds
if self.__torrent_peers: if self.__torrent_peers:
peers = etree.SubElement(entry, '{%s}peers' % TORRENT_NS) peers = etree.SubElement(entry, '{%s}peers' % TORRENT_NS)
peers.text = self.__torrent_peers peers.text = self.__torrent_peers
if self.__torrent_seeds: if self.__torrent_seeds:
verified = etree.SubElement(entry, '{%s}verified' % TORRENT_NS) verified = etree.SubElement(entry, '{%s}verified' % TORRENT_NS)
verified.text = self.__torrent_verified verified.text = self.__torrent_verified
def filename(self, torrent_filename=None):
'''Get or set the name of the torrent file.
def filename(self, torrent_filename=None): :param torrent_filename: The name of the torrent file.
'''Get or set the name of the torrent file. :returns: The name of the torrent file.
'''
if torrent_filename is not None:
self.__torrent_filename = torrent_filename
return self.__torrent_filename
:param torrent_filename: The name of the torrent file. def infohash(self, torrent_infohash=None):
:returns: The name of the torrent file. '''Get or set the hash of the target file.
'''
if not torrent_filename is None:
self.__torrent_filename = torrent_filename
return self.__torrent_filename
def infohash (self, torrent_infohash=None): :param torrent_infohash: The target file hash.
'''Get or set the hash of the target file. :returns: The target hash file.
'''
if torrent_infohash is not None:
self.__torrent_infohash = torrent_infohash
return self.__torrent_infohash
:param torrent_infohash: The target file hash. def contentlength(self, torrent_contentlength=None):
:returns: The target hash file. '''Get or set the size of the target file.
'''
if not torrent_infohash is None:
self.__torrent_infohash = torrent_infohash
return self.__torrent_infohash
def contentlength (self, torrent_contentlength=None): :param torrent_contentlength: The target file size.
'''Get or set the size of the target file. :returns: The target file size.
'''
if torrent_contentlength is not None:
self.__torrent_contentlength = torrent_contentlength
return self.__torrent_contentlength
:param torrent_contentlength: The target file size. def seeds(self, torrent_seeds=None):
:returns: The target file size. '''Get or set the number of seeds.
'''
if not torrent_contentlength is None:
self.__torrent_contentlength = torrent_contentlength
return self.__torrent_contentlength
def seeds (self, torrent_seeds=None): :param torrent_seeds: The seeds number.
'''Get or set the number of seeds. :returns: The seeds number.
'''
if torrent_seeds is not None:
self.__torrent_seeds = torrent_seeds
return self.__torrent_seeds
:param torrent_seeds: The seeds number. def peers(self, torrent_peers=None):
:returns: The seeds number. '''Get or set the number od peers
'''
if not torrent_seeds is None:
self.__torrent_seeds = torrent_seeds
return self.__torrent_seeds
def peers (self, torrent_peers=None): :param torrent_infohash: The peers number.
'''Get or set the number od peers :returns: The peers number.
'''
if torrent_peers is not None:
self.__torrent_peers = torrent_peers
return self.__torrent_peers
:param torrent_infohash: The peers number. def verified(self, torrent_verified=None):
:returns: The peers number. '''Get or set the number of verified peers.
'''
if not torrent_peers is None:
self.__torrent_peers = torrent_peers
return self.__torrent_peers
def verified (self, torrent_verified=None): :param torrent_infohash: The verified peers number.
'''Get or set the number of verified peers. :returns: The verified peers number.
'''
:param torrent_infohash: The verified peers number. if torrent_verified is not None:
:returns: The verified peers number. self.__torrent_verified = torrent_verified
''' return self.__torrent_verified
if not torrent_verified is None:
self.__torrent_verified = torrent_verified
return self.__torrent_verified

File diff suppressed because it is too large Load diff

View file

@ -1,72 +1,74 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
feedgen.util feedgen.util
~~~~~~~~~~~~ ~~~~~~~~~~~~
This file contains helper functions for the feed generator module. This file contains helper functions for the feed generator module.
:copyright: 2013, Lars Kiesow <lkiesow@uos.de> :copyright: 2013, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
import sys, locale import locale
import sys
def ensure_format(val, allowed, required, allowed_values=None, defaults=None): def ensure_format(val, allowed, required, allowed_values=None, defaults=None):
'''Takes a dictionary or a list of dictionaries and check if all keys are in '''Takes a dictionary or a list of dictionaries and check if all keys are in
the set of allowed keys, if all required keys are present and if the values the set of allowed keys, if all required keys are present and if the values
of a specific key are ok. of a specific key are ok.
:param val: Dictionaries to check. :param val: Dictionaries to check.
:param allowed: Set of allowed keys. :param allowed: Set of allowed keys.
:param required: Set of required keys. :param required: Set of required keys.
:param allowed_values: Dictionary with keys and sets of their allowed values. :param allowed_values: Dictionary with keys and sets of their allowed
:param defaults: Dictionary with default values. values.
:returns: List of checked dictionaries. :param defaults: Dictionary with default values.
''' :returns: List of checked dictionaries.
if not val: '''
return None if not val:
if allowed_values is None: return None
allowed_values = {} if allowed_values is None:
if defaults is None: allowed_values = {}
defaults = {} if defaults is None:
# Make shure that we have a list of dicts. Even if there is only one. defaults = {}
if not isinstance(val, list): # Make shure that we have a list of dicts. Even if there is only one.
val = [val] if not isinstance(val, list):
for elem in val: val = [val]
if not isinstance(elem, dict): for elem in val:
raise ValueError('Invalid data (value is no dictionary)') if not isinstance(elem, dict):
# Set default values raise ValueError('Invalid data (value is no dictionary)')
# Set default values
version = sys.version_info[0] version = sys.version_info[0]
if version == 2: if version == 2:
items = defaults.iteritems() items = defaults.iteritems()
else: else:
items = defaults.items() items = defaults.items()
for k,v in items: for k, v in items:
elem[k] = elem.get(k, v) elem[k] = elem.get(k, v)
if not set(elem.keys()) <= allowed: if not set(elem.keys()) <= allowed:
raise ValueError('Data contains invalid keys') raise ValueError('Data contains invalid keys')
if not set(elem.keys()) >= required: if not set(elem.keys()) >= required:
raise ValueError('Data contains not all required keys') raise ValueError('Data contains not all required keys')
if version == 2: if version == 2:
values = allowed_values.iteritems() values = allowed_values.iteritems()
else: else:
values = allowed_values.items() values = allowed_values.items()
for k,v in values: for k, v in values:
if elem.get(k) and not elem[k] in v: if elem.get(k) and not elem[k] in v:
raise ValueError('Invalid value for %s' % k ) raise ValueError('Invalid value for %s' % k)
return val return val
def formatRFC2822(d): def formatRFC2822(d):
'''Make sure the locale setting do not interfere with the time format. '''Make sure the locale setting do not interfere with the time format.
''' '''
l = locale.setlocale(locale.LC_ALL) l = locale.setlocale(locale.LC_ALL)
locale.setlocale(locale.LC_ALL, 'C') locale.setlocale(locale.LC_ALL, 'C')
d = d.strftime('%a, %d %b %Y %H:%M:%S %z') d = d.strftime('%a, %d %b %Y %H:%M:%S %z')
locale.setlocale(locale.LC_ALL, l) locale.setlocale(locale.LC_ALL, l)
return d return d

View file

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
''' '''
feedgen.version feedgen.version
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
:copyright: 2013-2017, Lars Kiesow <lkiesow@uos.de> :copyright: 2013-2017, Lars Kiesow <lkiesow@uos.de>
:license: FreeBSD and LGPL, see license.* for more details. :license: FreeBSD and LGPL, see license.* for more details.
''' '''
@ -18,8 +18,8 @@ version_str = '.'.join([str(x) for x in version])
version_major = version[:1] version_major = version[:1]
version_minor = version[:2] version_minor = version[:2]
version_full = version version_full = version
version_major_str = '.'.join([str(x) for x in version_major]) version_major_str = '.'.join([str(x) for x in version_major])
version_minor_str = '.'.join([str(x) for x in version_minor]) version_minor_str = '.'.join([str(x) for x in version_minor])
version_full_str = '.'.join([str(x) for x in version_full]) version_full_str = '.'.join([str(x) for x in version_full])

View file

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

View file

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

View file

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

View file

@ -12,288 +12,314 @@ from lxml import etree
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from feedgen.ext.dc import DcExtension, DcEntryExtension from feedgen.ext.dc import DcExtension, DcEntryExtension
class TestSequenceFunctions(unittest.TestCase): class TestSequenceFunctions(unittest.TestCase):
def setUp(self): def setUp(self):
fg = FeedGenerator() fg = FeedGenerator()
self.nsAtom = "http://www.w3.org/2005/Atom" self.nsAtom = "http://www.w3.org/2005/Atom"
self.nsRss = "http://purl.org/rss/1.0/modules/content/" self.nsRss = "http://purl.org/rss/1.0/modules/content/"
self.feedId = 'http://lernfunk.de/media/654321' self.feedId = 'http://lernfunk.de/media/654321'
self.title = 'Some Testfeed' self.title = 'Some Testfeed'
self.authorName = 'John Doe' self.authorName = 'John Doe'
self.authorMail = 'john@example.de' self.authorMail = 'john@example.de'
self.author = {'name': self.authorName,'email': self.authorMail} self.author = {'name': self.authorName, 'email': self.authorMail}
self.linkHref = 'http://example.com' self.linkHref = 'http://example.com'
self.linkRel = 'alternate' self.linkRel = 'alternate'
self.logo = 'http://ex.com/logo.jpg' self.logo = 'http://ex.com/logo.jpg'
self.subtitle = 'This is a cool feed!' self.subtitle = 'This is a cool feed!'
self.link2Href = 'http://larskiesow.de/test.atom' self.link2Href = 'http://larskiesow.de/test.atom'
self.link2Rel = 'self' self.link2Rel = 'self'
self.language = 'en' self.language = 'en'
self.categoryTerm = 'This category term' self.categoryTerm = 'This category term'
self.categoryScheme = 'This category scheme' self.categoryScheme = 'This category scheme'
self.categoryLabel = 'This category label' self.categoryLabel = 'This category label'
self.cloudDomain = 'example.com' self.cloudDomain = 'example.com'
self.cloudPort = '4711' self.cloudPort = '4711'
self.cloudPath = '/ws/example' self.cloudPath = '/ws/example'
self.cloudRegisterProcedure = 'registerProcedure' self.cloudRegisterProcedure = 'registerProcedure'
self.cloudProtocol = 'SOAP 1.1' self.cloudProtocol = 'SOAP 1.1'
self.icon = "http://example.com/icon.png" self.icon = "http://example.com/icon.png"
self.contributor = {'name':"Contributor Name", 'uri':"Contributor Uri", self.contributor = {'name': "Contributor Name",
'email': 'Contributor email'} 'uri': "Contributor Uri",
self.copyright = "The copyright notice" 'email': 'Contributor email'}
self.docs = 'http://www.rssboard.org/rss-specification' self.copyright = "The copyright notice"
self.managingEditor = 'mail@example.com' self.docs = 'http://www.rssboard.org/rss-specification'
self.rating = '(PICS-1.1 "http://www.classify.org/safesurf/" 1 r (SS~~000 1))' self.managingEditor = 'mail@example.com'
self.skipDays = 'Tuesday' self.rating = '(PICS-1.1 "http://www.classify.org/safesurf/" ' + \
self.skipHours = 23 '1 r (SS~~000 1))'
self.skipDays = 'Tuesday'
self.skipHours = 23
self.textInputTitle = "Text input title" self.textInputTitle = "Text input title"
self.textInputDescription = "Text input description" self.textInputDescription = "Text input description"
self.textInputName = "Text input name" self.textInputName = "Text input name"
self.textInputLink = "Text input link" self.textInputLink = "Text input link"
self.ttl = 900 self.ttl = 900
self.webMaster = 'webmaster@example.com' self.webMaster = 'webmaster@example.com'
fg.id(self.feedId) fg.id(self.feedId)
fg.title(self.title) fg.title(self.title)
fg.author(self.author) fg.author(self.author)
fg.link( href=self.linkHref, rel=self.linkRel ) fg.link(href=self.linkHref, rel=self.linkRel)
fg.logo(self.logo) fg.logo(self.logo)
fg.subtitle(self.subtitle) fg.subtitle(self.subtitle)
fg.link( href=self.link2Href, rel=self.link2Rel ) fg.link(href=self.link2Href, rel=self.link2Rel)
fg.language(self.language) fg.language(self.language)
fg.cloud(domain=self.cloudDomain, port=self.cloudPort, fg.cloud(domain=self.cloudDomain, port=self.cloudPort,
path=self.cloudPath, registerProcedure=self.cloudRegisterProcedure, path=self.cloudPath,
protocol=self.cloudProtocol) registerProcedure=self.cloudRegisterProcedure,
fg.icon(self.icon) protocol=self.cloudProtocol)
fg.category(term=self.categoryTerm, scheme=self.categoryScheme, fg.icon(self.icon)
label=self.categoryLabel) fg.category(term=self.categoryTerm, scheme=self.categoryScheme,
fg.contributor(self.contributor) label=self.categoryLabel)
fg.copyright(self.copyright) fg.contributor(self.contributor)
fg.docs(docs=self.docs) fg.copyright(self.copyright)
fg.managingEditor(self.managingEditor) fg.docs(docs=self.docs)
fg.rating(self.rating) fg.managingEditor(self.managingEditor)
fg.skipDays(self.skipDays) fg.rating(self.rating)
fg.skipHours(self.skipHours) fg.skipDays(self.skipDays)
fg.textInput(title=self.textInputTitle, fg.skipHours(self.skipHours)
description=self.textInputDescription, name=self.textInputName, fg.textInput(title=self.textInputTitle,
link=self.textInputLink) description=self.textInputDescription,
fg.ttl(self.ttl) name=self.textInputName, link=self.textInputLink)
fg.webMaster(self.webMaster) fg.ttl(self.ttl)
fg.webMaster(self.webMaster)
self.fg = fg self.fg = fg
def test_baseFeed(self):
fg = self.fg
def test_baseFeed(self): assert fg.id() == self.feedId
fg = self.fg assert fg.title() == self.title
assert fg.id() == self.feedId assert fg.author()[0]['name'] == self.authorName
assert fg.title() == self.title assert fg.author()[0]['email'] == self.authorMail
assert fg.author()[0]['name'] == self.authorName assert fg.link()[0]['href'] == self.linkHref
assert fg.author()[0]['email'] == self.authorMail assert fg.link()[0]['rel'] == self.linkRel
assert fg.link()[0]['href'] == self.linkHref assert fg.logo() == self.logo
assert fg.link()[0]['rel'] == self.linkRel assert fg.subtitle() == self.subtitle
assert fg.logo() == self.logo assert fg.link()[1]['href'] == self.link2Href
assert fg.subtitle() == self.subtitle assert fg.link()[1]['rel'] == self.link2Rel
assert fg.link()[1]['href'] == self.link2Href assert fg.language() == self.language
assert fg.link()[1]['rel'] == self.link2Rel
assert fg.language() == self.language def test_atomFeedFile(self):
fg = self.fg
filename = 'tmp_Atomfeed.xml'
fg.atom_file(filename=filename, pretty=True, xml_declaration=False)
def test_atomFeedFile(self): with open(filename, "r") as myfile:
fg = self.fg atomString = myfile.read().replace('\n', '')
filename = 'tmp_Atomfeed.xml'
fg.atom_file(filename=filename, pretty=True, xml_declaration=False)
with open (filename, "r") as myfile: self.checkAtomString(atomString)
atomString=myfile.read().replace('\n', '')
self.checkAtomString(atomString) def test_atomFeedString(self):
fg = self.fg
def test_atomFeedString(self): atomString = fg.atom_str(pretty=True, xml_declaration=False)
fg = self.fg self.checkAtomString(atomString)
atomString = fg.atom_str(pretty=True, xml_declaration=False) def test_rel_values_for_atom(self):
self.checkAtomString(atomString) 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]
fg = self.fg
fg.link(links, replace=True)
atomString = fg.atom_str(pretty=True, xml_declaration=False)
feed = etree.fromstring(atomString)
nsAtom = self.nsAtom
feed_links = feed.findall("{%s}link" % nsAtom)
idx = 0
assert len(links) == len(feed_links)
while idx < len(values_for_rel):
assert feed_links[idx].get('href') == links[idx]['href']
assert feed_links[idx].get('rel') == links[idx]['rel']
idx += 1
def test_rel_values_for_atom(self): def test_rel_values_for_rss(self):
values_for_rel = [ values_for_rel = [
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark', 'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark',
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form', 'canonical', 'chapter', 'collection', 'contents', 'copyright',
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure', 'create-form', 'current', 'derivedfrom', 'describedby',
'duplicate', 'edit', 'edit-form', 'edit-media', 'enclosure', 'first', 'glossary', 'describes', 'disclosure', 'duplicate', 'edit', 'edit-form',
'help', 'hosts', 'hub', 'icon', 'index', 'item', 'last', 'latest-version', 'license', 'edit-media', 'enclosure', 'first', 'glossary', 'help', 'hosts',
'lrdd', 'memento', 'monitor', 'monitor-group', 'next', 'next-archive', 'nofollow', 'hub', 'icon', 'index', 'item', 'last', 'latest-version',
'noreferrer', 'original', 'payment', 'predecessor-version', 'prefetch', 'prev', 'preview', 'license', 'lrdd', 'memento', 'monitor', 'monitor-group', 'next',
'previous', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies', 'search', 'next-archive', 'nofollow', 'noreferrer', 'original', 'payment',
'section', 'self', 'service', 'start', 'stylesheet', 'subsection', 'successor-version', 'predecessor-version', 'prefetch', 'prev', 'preview', 'previous',
'tag', 'terms-of-service', 'timegate', 'timemap', 'type', 'up', 'version-history', 'via', 'prev-archive', 'privacy-policy', 'profile', 'related', 'replies',
'working-copy', 'working-copy-of' 'search', 'section', 'self', 'service', 'start', 'stylesheet',
] 'subsection', 'successor-version', 'tag', 'terms-of-service',
links = [{'href': '%s/%s' % (self.linkHref, val.replace('-', '_')), 'rel': val} for val in values_for_rel] 'timegate', 'timemap', 'type', 'up', 'version-history', 'via',
fg = self.fg 'working-copy', 'working-copy-of']
fg.link(links, replace=True) links = [{'href': '%s/%s' % (self.linkHref,
atomString = fg.atom_str(pretty=True, xml_declaration=False) val.replace('-', '_')), 'rel': val}
feed = etree.fromstring(atomString) for val in values_for_rel]
nsAtom = self.nsAtom fg = self.fg
feed_links = feed.findall("{%s}link" % nsAtom) fg.link(links, replace=True)
idx = 0 rssString = fg.rss_str(pretty=True, xml_declaration=False)
assert len(links) == len(feed_links) feed = etree.fromstring(rssString)
while idx < len(values_for_rel): channel = feed.find("channel")
assert feed_links[idx].get('href') == links[idx]['href'] nsAtom = self.nsAtom
assert feed_links[idx].get('rel') == links[idx]['rel']
idx += 1
def test_rel_values_for_rss(self): atom_links = channel.findall("{%s}link" % nsAtom)
values_for_rel = [ # rss feed only implements atom's 'self' link
'about', 'alternate', 'appendix', 'archives', 'author', 'bookmark', assert len(atom_links) == 1
'canonical', 'chapter', 'collection', 'contents', 'copyright', 'create-form', assert atom_links[0].get('href') == '%s/%s' % (self.linkHref, 'self')
'current', 'derivedfrom', 'describedby', 'describes', 'disclosure', assert atom_links[0].get('rel') == 'self'
'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)
feed = etree.fromstring(rssString)
channel = feed.find("channel")
nsAtom = self.nsAtom
atom_links = channel.findall("{%s}link" % nsAtom) rss_links = channel.findall('link')
assert len(atom_links) == 1 # rss feed only implements atom's 'self' link # RSS only needs one URL. We use the first link for RSS:
assert atom_links[0].get('href') == '%s/%s' % (self.linkHref, 'self') assert len(rss_links) == 1
assert atom_links[0].get('rel') == 'self' assert rss_links[0].text == '%s/%s' % \
(self.linkHref, 'working-copy-of'.replace('-', '_'))
rss_links = channel.findall('link') def checkAtomString(self, atomString):
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('-', '_'))
def checkAtomString(self, atomString): feed = etree.fromstring(atomString)
feed = etree.fromstring(atomString) nsAtom = self.nsAtom
nsAtom = self.nsAtom assert feed.find("{%s}title" % nsAtom).text == self.title
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.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('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}rights" % nsAtom).text == self.copyright
assert feed.find("{%s}title" % nsAtom).text == self.title def test_rssFeedFile(self):
assert feed.find("{%s}updated" % nsAtom).text != None fg = self.fg
assert feed.find("{%s}id" % nsAtom).text == self.feedId filename = 'tmp_Rssfeed.xml'
assert feed.find("{%s}category" % nsAtom).get('term') == self.categoryTerm fg.rss_file(filename=filename, pretty=True, xml_declaration=False)
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('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}rights" % nsAtom).text == self.copyright
def test_rssFeedFile(self): with open(filename, "r") as myfile:
fg = self.fg rssString = myfile.read().replace('\n', '')
filename = 'tmp_Rssfeed.xml'
fg.rss_file(filename=filename, pretty=True, xml_declaration=False)
with open (filename, "r") as myfile: self.checkRssString(rssString)
rssString=myfile.read().replace('\n', '')
self.checkRssString(rssString) def test_rssFeedString(self):
fg = self.fg
rssString = fg.rss_str(pretty=True, xml_declaration=False)
self.checkRssString(rssString)
def test_rssFeedString(self): def test_loadPodcastExtension(self):
fg = self.fg fg = self.fg
rssString = fg.rss_str(pretty=True, xml_declaration=False) fg.add_entry()
self.checkRssString(rssString) fg.load_extension('podcast', atom=True, rss=True)
fg.add_entry()
def test_loadPodcastExtension(self): def test_loadDcExtension(self):
fg = self.fg fg = self.fg
fg.add_entry() fg.add_entry()
fg.load_extension('podcast', atom=True, rss=True) fg.load_extension('dc', atom=True, rss=True)
fg.add_entry() fg.add_entry()
def test_loadDcExtension(self): def test_extensionAlreadyLoaded(self):
fg = self.fg fg = self.fg
fg.add_entry() fg.load_extension('dc', atom=True, rss=True)
fg.load_extension('dc', atom=True, rss=True) with self.assertRaises(ImportError):
fg.add_entry() fg.load_extension('dc')
def test_extensionAlreadyLoaded(self): def test_registerCustomExtension(self):
fg = self.fg fg = self.fg
fg.load_extension('dc', atom=True, rss=True) fg.add_entry()
with self.assertRaises(ImportError) as context: fg.register_extension('dc', DcExtension, DcEntryExtension)
fg.load_extension('dc') fg.add_entry()
def test_registerCustomExtension(self): def checkRssString(self, rssString):
fg = self.fg
fg.add_entry()
fg.register_extension('dc', DcExtension, DcEntryExtension)
fg.add_entry()
def checkRssString(self, rssString): feed = etree.fromstring(rssString)
nsAtom = self.nsAtom
feed = etree.fromstring(rssString) ch = feed.find("channel")
nsAtom = self.nsAtom assert ch is not None
nsRss = self.nsRss
channel = feed.find("channel") assert ch.find("title").text == self.title
assert channel != None assert ch.find("description").text == self.subtitle
assert ch.find("lastBuildDate").text is not None
docs = "http://www.rssboard.org/rss-specification"
assert ch.find("docs").text == docs
assert ch.find("generator").text == "python-feedgen"
assert ch.findall("{%s}link" % nsAtom)[0].get('href') == self.link2Href
assert ch.findall("{%s}link" % nsAtom)[0].get('rel') == self.link2Rel
assert ch.find("image").find("url").text == self.logo
assert ch.find("image").find("title").text == self.title
assert ch.find("image").find("link").text == self.link2Href
assert ch.find("category").text == self.categoryLabel
assert ch.find("cloud").get('domain') == self.cloudDomain
assert ch.find("cloud").get('port') == self.cloudPort
assert ch.find("cloud").get('path') == self.cloudPath
assert ch.find("cloud").get('registerProcedure') == \
self.cloudRegisterProcedure
assert ch.find("cloud").get('protocol') == self.cloudProtocol
assert ch.find("copyright").text == self.copyright
assert ch.find("docs").text == self.docs
assert ch.find("managingEditor").text == self.managingEditor
assert ch.find("rating").text == self.rating
assert ch.find("skipDays").find("day").text == self.skipDays
assert int(ch.find("skipHours").find("hour").text) == self.skipHours
assert ch.find("textInput").get('title') == self.textInputTitle
assert ch.find("textInput").get('description') == \
self.textInputDescription
assert ch.find("textInput").get('name') == self.textInputName
assert ch.find("textInput").get('link') == self.textInputLink
assert int(ch.find("ttl").text) == self.ttl
assert ch.find("webMaster").text == self.webMaster
assert channel.find("title").text == self.title
assert channel.find("description").text == self.subtitle
assert channel.find("lastBuildDate").text != None
assert channel.find("docs").text == "http://www.rssboard.org/rss-specification"
assert channel.find("generator").text == "python-feedgen"
assert channel.findall("{%s}link" % nsAtom)[0].get('href') == self.link2Href
assert channel.findall("{%s}link" % nsAtom)[0].get('rel') == self.link2Rel
assert channel.find("image").find("url").text == self.logo
assert channel.find("image").find("title").text == self.title
assert channel.find("image").find("link").text == self.link2Href
assert channel.find("category").text == self.categoryLabel
assert channel.find("cloud").get('domain') == self.cloudDomain
assert channel.find("cloud").get('port') == self.cloudPort
assert channel.find("cloud").get('path') == self.cloudPath
assert channel.find("cloud").get('registerProcedure') == self.cloudRegisterProcedure
assert channel.find("cloud").get('protocol') == self.cloudProtocol
assert channel.find("copyright").text == self.copyright
assert channel.find("docs").text == self.docs
assert channel.find("managingEditor").text == self.managingEditor
assert channel.find("rating").text == self.rating
assert channel.find("skipDays").find("day").text == self.skipDays
assert int(channel.find("skipHours").find("hour").text) == self.skipHours
assert channel.find("textInput").get('title') == self.textInputTitle
assert channel.find("textInput").get('description') == self.textInputDescription
assert channel.find("textInput").get('name') == self.textInputName
assert channel.find("textInput").get('link') == self.textInputLink
assert int(channel.find("ttl").text) == self.ttl
assert channel.find("webMaster").text == self.webMaster
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()