upgraded feedparer and PyRSS2Gen

This commit is contained in:
mdipierro
2012-10-19 12:59:58 -05:00
parent 6aa5edc7ff
commit c7943b8977
3 changed files with 890 additions and 876 deletions
+1 -1
View File
@@ -1 +1 @@
Version 2.1.1 (2012-10-19 12:33:48) dev Version 2.1.1 (2012-10-19 12:59:55) dev
File diff suppressed because it is too large Load Diff
+185 -251
View File
@@ -1,58 +1,95 @@
#!/usr/bin/env python """
# -*- coding: utf-8 -*- PyRSS2Gen - A Python library for generating RSS 2.0 feeds.
"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds.""" (This is the BSD license, based on the template at
http://www.opensource.org/licenses/bsd-license.php )
__name__ = 'PyRSS2Gen' Copyright (c) 2003, Dalke Scientific Software, LLC
__version__ = (1, 0, 0)
__author__ = 'Andrew Dalke <dalke@dalkescientific.com>'
_generator_name = __name__ + '-' + '.'.join(map(str, __version__)) All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of the Dalke Scientific Softare, LLC, Andrew
Dalke, nor the names of its contributors may be used to endorse or
promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
__name__ = "PyRSS2Gen"
__version__ = (1, 1, 0)
__author__ = "Andrew Dalke <dalke@dalkescientific.com>"
_generator_name = __name__ + "-" + ".".join(map(str, __version__))
import datetime import datetime
import cStringIO
import sys
if sys.version_info[0] == 3:
# Python 3
basestring = str
from io import StringIO
else:
# Python 2
try:
from cStringIO import StringIO
except ImportError:
# Very old (or memory constrained) systems might
# have left out the compiled C version. Fall back
# to the pure Python one. Haven't seen this sort
# of system since the early 2000s.
from StringIO import StringIO
# Could make this the base class; will need to add 'publish' # Could make this the base class; will need to add 'publish'
class WriteXmlMixin: class WriteXmlMixin:
def write_xml(self, outfile, encoding="iso-8859-1"):
def write_xml(self, outfile, encoding='iso-8859-1'):
from xml.sax import saxutils from xml.sax import saxutils
handler = saxutils.XMLGenerator(outfile, encoding) handler = saxutils.XMLGenerator(outfile, encoding)
handler.startDocument() handler.startDocument()
self.publish(handler) self.publish(handler)
handler.endDocument() handler.endDocument()
def to_xml(self, encoding='iso-8859-1'): def to_xml(self, encoding="iso-8859-1"):
try: f = StringIO()
import cStringIO as StringIO
except ImportError:
import StringIO
f = StringIO.StringIO()
self.write_xml(f, encoding) self.write_xml(f, encoding)
return f.getvalue() return f.getvalue()
def _element( def _element(handler, name, obj, d={}):
handler,
name,
obj,
d={},
):
if isinstance(obj, basestring) or obj is None: if isinstance(obj, basestring) or obj is None:
# special-case handling to make the API easier # special-case handling to make the API easier
# to use for the common case. # to use for the common case.
handler.startElement(name, d) handler.startElement(name, d)
if obj is not None: if obj is not None:
handler.characters(obj) handler.characters(obj)
handler.endElement(name) handler.endElement(name)
else: else:
# It better know how to emit the correct XML. # It better know how to emit the correct XML.
obj.publish(handler) obj.publish(handler)
@@ -67,7 +104,6 @@ def _format_date(dt):
Input date must be in GMT. Input date must be in GMT.
""" """
# Looks like: # Looks like:
# Sat, 07 Sep 2002 00:00:01 GMT # Sat, 07 Sep 2002 00:00:01 GMT
# Can't use strftime because that's locale dependent # Can't use strftime because that's locale dependent
@@ -75,46 +111,18 @@ def _format_date(dt):
# Isn't there a standard way to do this for Python? The # Isn't there a standard way to do this for Python? The
# rfc822 and email.Utils modules assume a timestamp. The # rfc822 and email.Utils modules assume a timestamp. The
# following is based on the rfc822 module. # following is based on the rfc822 module.
return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (
return '%s, %02d %s %04d %02d:%02d:%02d GMT' % ( ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()],
[
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
'Sun',
][dt.weekday()],
dt.day, dt.day,
[ ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
'Jan', "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month - 1],
'Feb', dt.year, dt.hour, dt.minute, dt.second)
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
][dt.month - 1],
dt.year,
dt.hour,
dt.minute,
dt.second,
)
## ##
# A couple simple wrapper objects for the fields which # A couple simple wrapper objects for the fields which
# take a simple value other than a string. # take a simple value other than a string.
class IntElement: class IntElement:
"""implements the 'publish' API for integers """implements the 'publish' API for integers
Takes the tag name and the integer value to publish. Takes the tag name and the integer value to publish.
@@ -122,7 +130,6 @@ class IntElement:
(Could be used for anything which uses str() to be published (Could be used for anything which uses str() to be published
to text for XML.) to text for XML.)
""" """
element_attrs = {} element_attrs = {}
def __init__(self, name, val): def __init__(self, name, val):
@@ -136,29 +143,23 @@ class IntElement:
class DateElement: class DateElement:
"""implements the 'publish' API for a datetime.datetime """implements the 'publish' API for a datetime.datetime
Takes the tag name and the datetime to publish. Takes the tag name and the datetime to publish.
Converts the datetime to RFC 2822 timestamp (4-digit year). Converts the datetime to RFC 2822 timestamp (4-digit year).
""" """
def __init__(self, name, dt): def __init__(self, name, dt):
self.name = name self.name = name
self.dt = dt self.dt = dt
def publish(self, handler): def publish(self, handler):
_element(handler, self.name, _format_date(self.dt)) _element(handler, self.name, _format_date(self.dt))
####
# ###
class Category: class Category:
"""Publish a category element""" """Publish a category element"""
def __init__(self, category, domain=None): def __init__(self, category, domain=None):
self.category = category self.category = category
self.domain = domain self.domain = domain
@@ -166,22 +167,14 @@ class Category:
def publish(self, handler): def publish(self, handler):
d = {} d = {}
if self.domain is not None: if self.domain is not None:
d['domain'] = self.domain d["domain"] = self.domain
_element(handler, 'category', self.category, d) _element(handler, "category", self.category, d)
class Cloud: class Cloud:
"""Publish a cloud""" """Publish a cloud"""
def __init__(self, domain, port, path,
def __init__( registerProcedure, protocol):
self,
domain,
port,
path,
registerProcedure,
protocol,
):
self.domain = domain self.domain = domain
self.port = port self.port = port
self.path = path self.path = path
@@ -189,30 +182,20 @@ class Cloud:
self.protocol = protocol self.protocol = protocol
def publish(self, handler): def publish(self, handler):
_element(handler, 'cloud', None, { _element(handler, "cloud", None, {
'domain': self.domain, "domain": self.domain,
'port': str(self.port), "port": str(self.port),
'path': self.path, "path": self.path,
'registerProcedure': self.registerProcedure, "registerProcedure": self.registerProcedure,
'protocol': self.protocol, "protocol": self.protocol})
})
class Image: class Image:
"""Publish a channel Image""" """Publish a channel Image"""
element_attrs = {} element_attrs = {}
def __init__( def __init__(self, url, title, link,
self, width=None, height=None, description=None):
url,
title,
link,
width=None,
height=None,
description=None,
):
self.url = url self.url = url
self.title = title self.title = title
self.link = link self.link = link
@@ -221,35 +204,33 @@ class Image:
self.description = description self.description = description
def publish(self, handler): def publish(self, handler):
handler.startElement('image', self.element_attrs) handler.startElement("image", self.element_attrs)
_element(handler, 'url', self.url) _element(handler, "url", self.url)
_element(handler, 'title', self.title) _element(handler, "title", self.title)
_element(handler, 'link', self.link) _element(handler, "link", self.link)
width = self.width width = self.width
if isinstance(width, int): if isinstance(width, int):
width = IntElement('width', width) width = IntElement("width", width)
_opt_element(handler, 'width', width) _opt_element(handler, "width", width)
height = self.height height = self.height
if isinstance(height, int): if isinstance(height, int):
height = IntElement('height', height) height = IntElement("height", height)
_opt_element(handler, 'height', height) _opt_element(handler, "height", height)
_opt_element(handler, 'description', self.description) _opt_element(handler, "description", self.description)
handler.endElement('image') handler.endElement("image")
class Guid: class Guid:
"""Publish a guid """Publish a guid
Defaults to being a permalink, which is the assumption if it's Defaults to being a permalink, which is the assumption if it's
omitted. Hence strings are always permalinks. omitted. Hence strings are always permalinks.
""" """
def __init__(self, guid, isPermaLink=1): def __init__(self, guid, isPermaLink=1):
self.guid = guid self.guid = guid
self.isPermaLink = isPermaLink self.isPermaLink = isPermaLink
@@ -257,80 +238,64 @@ class Guid:
def publish(self, handler): def publish(self, handler):
d = {} d = {}
if self.isPermaLink: if self.isPermaLink:
d['isPermaLink'] = 'true' d["isPermaLink"] = "true"
else: else:
d['isPermaLink'] = 'false' d["isPermaLink"] = "false"
_element(handler, 'guid', self.guid, d) _element(handler, "guid", self.guid, d)
class TextInput: class TextInput:
"""Publish a textInput """Publish a textInput
Apparently this is rarely used. Apparently this is rarely used.
""" """
element_attrs = {} element_attrs = {}
def __init__( def __init__(self, title, description, name, link):
self,
title,
description,
name,
link,
):
self.title = title self.title = title
self.description = description self.description = description
self.name = name self.name = name
self.link = link self.link = link
def publish(self, handler): def publish(self, handler):
handler.startElement('textInput', self.element_attrs) handler.startElement("textInput", self.element_attrs)
_element(handler, 'title', self.title) _element(handler, "title", self.title)
_element(handler, 'description', self.description) _element(handler, "description", self.description)
_element(handler, 'name', self.name) _element(handler, "name", self.name)
_element(handler, 'link', self.link) _element(handler, "link", self.link)
handler.endElement('textInput') handler.endElement("textInput")
class Enclosure: class Enclosure:
"""Publish an enclosure""" """Publish an enclosure"""
def __init__(self, url, length, type):
def __init__(
self,
url,
length,
type,
):
self.url = url self.url = url
self.length = length self.length = length
self.type = type self.type = type
def publish(self, handler): def publish(self, handler):
_element(handler, 'enclosure', None, _element(handler, "enclosure", None,
{'url': self.url, 'length': str(self.length), 'type': self.type}) {"url": self.url,
"length": str(self.length),
"type": self.type,
})
class Source: class Source:
"""Publish the item's original source, used by aggregators""" """Publish the item's original source, used by aggregators"""
def __init__(self, name, url): def __init__(self, name, url):
self.name = name self.name = name
self.url = url self.url = url
def publish(self, handler): def publish(self, handler):
_element(handler, 'source', self.name, {'url': self.url}) _element(handler, "source", self.name, {"url": self.url})
class SkipHours: class SkipHours:
"""Publish the skipHours """Publish the skipHours
This takes a list of hours, as integers. This takes a list of hours, as integers.
""" """
element_attrs = {} element_attrs = {}
def __init__(self, hours): def __init__(self, hours):
@@ -338,19 +303,17 @@ class SkipHours:
def publish(self, handler): def publish(self, handler):
if self.hours: if self.hours:
handler.startElement('skipHours', self.element_attrs) handler.startElement("skipHours", self.element_attrs)
for hour in self.hours: for hour in self.hours:
_element(handler, 'hour', str(hour)) _element(handler, "hour", str(hour))
handler.endElement('skipHours') handler.endElement("skipHours")
class SkipDays: class SkipDays:
"""Publish the skipDays """Publish the skipDays
This takes a list of days as strings. This takes a list of days as strings.
""" """
element_attrs = {} element_attrs = {}
def __init__(self, days): def __init__(self, days):
@@ -358,47 +321,48 @@ class SkipDays:
def publish(self, handler): def publish(self, handler):
if self.days: if self.days:
handler.startElement('skipDays', self.element_attrs) handler.startElement("skipDays", self.element_attrs)
for day in self.days: for day in self.days:
_element(handler, 'day', day) _element(handler, "day", day)
handler.endElement('skipDays') handler.endElement("skipDays")
class RSS2(WriteXmlMixin): class RSS2(WriteXmlMixin):
"""The main RSS class. """The main RSS class.
Stores the channel attributes, with the \"category\" elements under Stores the channel attributes, with the "category" elements under
\".categories\" and the RSS items under \".items\". ".categories" and the RSS items under ".items".
""" """
rss_attrs = {'version': '2.0'} rss_attrs = {"version": "2.0"}
element_attrs = {} element_attrs = {}
def __init__( def __init__(self,
self, title,
title, link,
link, description,
description,
language=None,
copyright=None,
managingEditor=None,
webMaster=None,
pubDate=None,
lastBuildDate=None,
categories=None,
generator=_generator_name,
docs='http://blogs.law.harvard.edu/tech/rss',
cloud=None,
ttl=None,
image=None,
rating=None,
textInput=None,
skipHours=None,
skipDays=None,
items=None,
):
language=None,
copyright=None,
managingEditor=None,
webMaster=None,
pubDate=None, # a datetime, *in* *GMT*
lastBuildDate=None, # a datetime
categories=None, # list of strings or Category
generator=_generator_name,
docs="http://blogs.law.harvard.edu/tech/rss",
cloud=None, # a Cloud
ttl=None, # integer number of minutes
image=None, # an Image
rating=None, # a string; I don't know how it's used
textInput=None, # a TextInput
skipHours=None, # a SkipHours with a list of integers
skipDays=None, # a SkipDays with a list of strings
items=None, # list of RSSItems
):
self.title = title self.title = title
self.link = link self.link = link
self.description = description self.description = description
@@ -428,49 +392,49 @@ class RSS2(WriteXmlMixin):
self.items = items self.items = items
def publish(self, handler): def publish(self, handler):
handler.startElement('rss', self.rss_attrs) handler.startElement("rss", self.rss_attrs)
handler.startElement('channel', self.element_attrs) handler.startElement("channel", self.element_attrs)
_element(handler, 'title', self.title) _element(handler, "title", self.title)
_element(handler, 'link', self.link) _element(handler, "link", self.link)
_element(handler, 'description', self.description) _element(handler, "description", self.description)
self.publish_extensions(handler) self.publish_extensions(handler)
_opt_element(handler, 'language', self.language) _opt_element(handler, "language", self.language)
_opt_element(handler, 'copyright', self.copyright) _opt_element(handler, "copyright", self.copyright)
_opt_element(handler, 'managingEditor', self.managingEditor) _opt_element(handler, "managingEditor", self.managingEditor)
_opt_element(handler, 'webMaster', self.webMaster) _opt_element(handler, "webMaster", self.webMaster)
pubDate = self.pubDate pubDate = self.pubDate
if isinstance(pubDate, datetime.datetime): if isinstance(pubDate, datetime.datetime):
pubDate = DateElement('pubDate', pubDate) pubDate = DateElement("pubDate", pubDate)
_opt_element(handler, 'pubDate', pubDate) _opt_element(handler, "pubDate", pubDate)
lastBuildDate = self.lastBuildDate lastBuildDate = self.lastBuildDate
if isinstance(lastBuildDate, datetime.datetime): if isinstance(lastBuildDate, datetime.datetime):
lastBuildDate = DateElement('lastBuildDate', lastBuildDate) lastBuildDate = DateElement("lastBuildDate", lastBuildDate)
_opt_element(handler, 'lastBuildDate', lastBuildDate) _opt_element(handler, "lastBuildDate", lastBuildDate)
for category in self.categories: for category in self.categories:
if isinstance(category, basestring): if isinstance(category, basestring):
category = Category(category) category = Category(category)
category.publish(handler) category.publish(handler)
_opt_element(handler, 'generator', self.generator) _opt_element(handler, "generator", self.generator)
_opt_element(handler, 'docs', self.docs) _opt_element(handler, "docs", self.docs)
if self.cloud is not None: if self.cloud is not None:
self.cloud.publish(handler) self.cloud.publish(handler)
ttl = self.ttl ttl = self.ttl
if isinstance(self.ttl, int): if isinstance(self.ttl, int):
ttl = IntElement('ttl', ttl) ttl = IntElement("ttl", ttl)
_opt_element(handler, 'tt', ttl) _opt_element(handler, "ttl", ttl)
if self.image is not None: if self.image is not None:
self.image.publish(handler) self.image.publish(handler)
_opt_element(handler, 'rating', self.rating) _opt_element(handler, "rating", self.rating)
if self.textInput is not None: if self.textInput is not None:
self.textInput.publish(handler) self.textInput.publish(handler)
if self.skipHours is not None: if self.skipHours is not None:
@@ -481,40 +445,35 @@ class RSS2(WriteXmlMixin):
for item in self.items: for item in self.items:
item.publish(handler) item.publish(handler)
handler.endElement('channel') handler.endElement("channel")
handler.endElement('rss') handler.endElement("rss")
def publish_extensions(self, handler): def publish_extensions(self, handler):
# Derived classes can hook into this to insert # Derived classes can hook into this to insert
# output after the three required fields. # output after the three required fields.
pass pass
class RSSItem(WriteXmlMixin): class RSSItem(WriteXmlMixin):
"""Publish an RSS Item""" """Publish an RSS Item"""
element_attrs = {} element_attrs = {}
def __init__( def __init__(self,
self, title=None, # string
title=None, link=None, # url as string
link=None, description=None, # string
description=None, author=None, # email address as string
author=None, categories=None, # list of string or Category
categories=None, comments=None, # url as string
comments=None, enclosure=None, # an Enclosure
enclosure=None, guid=None, # a unique string
guid=None, pubDate=None, # a datetime
pubDate=None, source=None, # a Source
source=None, ):
):
if title is None and description is None: if title is None and description is None:
raise TypeError( raise TypeError(
"RSSItem must define at least one of 'title' or 'description'") "must define at least one of 'title' or 'description'")
self.title = title self.title = title
self.link = link self.link = link
self.description = description self.description = description
@@ -527,62 +486,37 @@ class RSSItem(WriteXmlMixin):
self.guid = guid self.guid = guid
self.pubDate = pubDate self.pubDate = pubDate
self.source = source self.source = source
# It sure does get tedious typing these names three times... # It sure does get tedious typing these names three times...
def publish(self, handler): def publish(self, handler):
handler.startElement('item', self.element_attrs) handler.startElement("item", self.element_attrs)
_opt_element(handler, 'title', self.title) _opt_element(handler, "title", self.title)
_opt_element(handler, 'link', self.link) _opt_element(handler, "link", self.link)
self.publish_extensions(handler) self.publish_extensions(handler)
_opt_element(handler, 'description', self.description) _opt_element(handler, "description", self.description)
_opt_element(handler, 'author', self.author) _opt_element(handler, "author", self.author)
for category in self.categories: for category in self.categories:
if isinstance(category, basestring): if isinstance(category, basestring):
category = Category(category) category = Category(category)
category.publish(handler) category.publish(handler)
_opt_element(handler, 'comments', self.comments) _opt_element(handler, "comments", self.comments)
if self.enclosure is not None: if self.enclosure is not None:
self.enclosure.publish(handler) self.enclosure.publish(handler)
_opt_element(handler, 'guid', self.guid) _opt_element(handler, "guid", self.guid)
pubDate = self.pubDate pubDate = self.pubDate
if isinstance(pubDate, datetime.datetime): if isinstance(pubDate, datetime.datetime):
pubDate = DateElement('pubDate', pubDate) pubDate = DateElement("pubDate", pubDate)
_opt_element(handler, 'pubDate', pubDate) _opt_element(handler, "pubDate", pubDate)
if self.source is not None: if self.source is not None:
self.source.publish(handler) self.source.publish(handler)
handler.endElement('item') handler.endElement("item")
def publish_extensions(self, handler): def publish_extensions(self, handler):
# Derived classes can hook into this to insert # Derived classes can hook into this to insert
# output after the title and link elements # output after the title and link elements
pass pass
def dumps(rss, encoding='utf-8'):
s = cStringIO.StringIO()
rss.write_xml(s, encoding)
return s.getvalue()
def test():
rss = RSS2(title='web2py feed', link='http://www.web2py.com',
description='About web2py',
lastBuildDate=datetime.datetime.now(),
items=[RSSItem(title='web2py and PyRSS2Gen-0.0',
link='http://www.web2py.com/examples/simple_examples/getrss',
description='web2py can now make rss feeds!',
guid=Guid('http://www.web2py.com/'),
pubDate=datetime.datetime(2007, 11, 14, 10, 30))])
return dumps(rss)
if __name__ == '__main__':
print test()