Back to index

moin  1.9.0~rc2
atom.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 """
00003     werkzeug.contrib.atom
00004     ~~~~~~~~~~~~~~~~~~~~~
00005 
00006     This module provides a class called :class:`AtomFeed` which can be
00007     used to generate feeds in the Atom syndication format (see :rfc:`4287`).
00008 
00009     Example::
00010 
00011         def atom_feed(request):
00012             feed = AtomFeed("My Blog", feed_url=request.url,
00013                             url=request.host_url,
00014                             subtitle="My example blog for a feed test.")
00015             for post in Post.query.limit(10).all():
00016                 feed.add(post.title, post.body, content_type='html',
00017                          author=post.author, url=post.url, id=post.uid,
00018                          updated=post.last_update, published=post.pub_date)
00019             return feed.get_response()
00020 
00021     :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
00022     :license: BSD, see LICENSE for more details.
00023 """
00024 from datetime import datetime
00025 from werkzeug.utils import escape
00026 from werkzeug.wrappers import BaseResponse
00027 
00028 
00029 XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'
00030 
00031 
00032 def _make_text_block(name, content, content_type=None):
00033     """Helper function for the builder that creates an XML text block."""
00034     if content_type == 'xhtml':
00035         return u'<%s type="xhtml"><div xmlns="%s">%s</div></%s>\n' % \
00036                (name, XHTML_NAMESPACE, content, name)
00037     if not content_type:
00038         return u'<%s>%s</%s>\n' % (name, escape(content), name)
00039     return u'<%s type="%s">%s</%s>\n' % (name, content_type,
00040                                          escape(content), name)
00041 
00042 
00043 def format_iso8601(obj):
00044     """Format a datetime object for iso8601"""
00045     return obj.strftime('%Y-%m-%dT%H:%M:%SZ')
00046 
00047 
00048 class AtomFeed(object):
00049     """A helper class that creates Atom feeds.
00050 
00051     :param title: the title of the feed. Required.
00052     :param title_type: the type attribute for the title element.  One of
00053                        ``'html'``, ``'text'`` or ``'xhtml'``.
00054     :param url: the url for the feed (not the url *of* the feed)
00055     :param id: a globally unique id for the feed.  Must be an URI.  If
00056                not present the `feed_url` is used, but one of both is
00057                required.
00058     :param updated: the time the feed was modified the last time.  Must
00059                     be a :class:`datetime.datetime` object.  If not
00060                     present the latest entry's `updated` is used.
00061     :param feed_url: the URL to the feed.  Should be the URL that was
00062                      requested.
00063     :param author: the author of the feed.  Must be either a string (the
00064                    name) or a dict with name (required) and uri or
00065                    email (both optional).  Can be a list of (may be
00066                    mixed, too) strings and dicts, too, if there are
00067                    multiple authors. Required if not every entry has an
00068                    author element.
00069     :param icon: an icon for the feed.
00070     :param logo: a logo for the feed.
00071     :param rights: copyright information for the feed.
00072     :param rights_type: the type attribute for the rights element.  One of
00073                         ``'html'``, ``'text'`` or ``'xhtml'``.  Default is
00074                         ``'text'``.
00075     :param subtitle: a short description of the feed.
00076     :param subtitle_type: the type attribute for the subtitle element.
00077                           One of ``'text'``, ``'html'``, ``'text'``
00078                           or ``'xhtml'``.  Default is ``'text'``.
00079     :param links: additional links.  Must be a list of dictionaries with
00080                   href (required) and rel, type, hreflang, title, length
00081                   (all optional)
00082     :param generator: the software that generated this feed.  This must be
00083                       a tuple in the form ``(name, url, version)``.  If
00084                       you don't want to specify one of them, set the item
00085                       to `None`.
00086     :param entries: a list with the entries for the feed. Entries can also
00087                     be added later with :meth:`add`.
00088 
00089     For more information on the elements see
00090     http://www.atomenabled.org/developers/syndication/
00091 
00092     Everywhere where a list is demanded, any iterable can be used.
00093     """
00094 
00095     default_generator = ('Werkzeug', None, None)
00096 
00097     def __init__(self, title=None, entries=None, **kwargs):
00098         self.title = title
00099         self.title_type = kwargs.get('title_type', 'text')
00100         self.url = kwargs.get('url')
00101         self.feed_url = kwargs.get('feed_url', self.url)
00102         self.id = kwargs.get('id', self.feed_url)
00103         self.updated = kwargs.get('updated')
00104         self.author = kwargs.get('author', ())
00105         self.icon = kwargs.get('icon')
00106         self.logo = kwargs.get('logo')
00107         self.rights = kwargs.get('rights')
00108         self.rights_type = kwargs.get('rights_type')
00109         self.subtitle = kwargs.get('subtitle')
00110         self.subtitle_type = kwargs.get('subtitle_type', 'text')
00111         self.generator = kwargs.get('generator')
00112         if self.generator is None:
00113             self.generator = self.default_generator
00114         self.links = kwargs.get('links', [])
00115         self.entries = entries and list(entries) or []
00116 
00117         if not hasattr(self.author, '__iter__') \
00118            or isinstance(self.author, (basestring, dict)):
00119             self.author = [self.author]
00120         for i, author in enumerate(self.author):
00121             if not isinstance(author, dict):
00122                 self.author[i] = {'name': author}
00123 
00124         if not self.title:
00125             raise ValueError('title is required')
00126         if not self.id:
00127             raise ValueError('id is required')
00128         for author in self.author:
00129             if 'name' not in author:
00130                 raise TypeError('author must contain at least a name')
00131 
00132     def add(self, *args, **kwargs):
00133         """Add a new entry to the feed.  This function can either be called
00134         with a :class:`FeedEntry` or some keyword and positional arguments
00135         that are forwarded to the :class:`FeedEntry` constructor.
00136         """
00137         if len(args) == 1 and not kwargs and isinstance(args[0], FeedEntry):
00138             self.entries.append(args[0])
00139         else:
00140             kwargs['feed_url'] = self.feed_url
00141             self.entries.append(FeedEntry(*args, **kwargs))
00142 
00143     def __repr__(self):
00144         return '<%s %r (%d entries)>' % (
00145             self.__class__.__name__,
00146             self.title,
00147             len(self.entries)
00148         )
00149 
00150     def generate(self):
00151         """Return a generator that yields pieces of XML."""
00152         # atom demands either an author element in every entry or a global one
00153         if not self.author:
00154             if False in map(lambda e: bool(e.author), self.entries):
00155                 self.author = ({'name': u'unbekannter Autor'},)
00156 
00157         if not self.updated:
00158             dates = sorted([entry.updated for entry in self.entries])
00159             self.updated = dates and dates[-1] or datetime.utcnow()
00160 
00161         yield u'<?xml version="1.0" encoding="utf-8"?>\n'
00162         yield u'<feed xmlns="http://www.w3.org/2005/Atom">\n'
00163         yield '  ' + _make_text_block('title', self.title, self.title_type)
00164         yield u'  <id>%s</id>\n' % escape(self.id)
00165         yield u'  <updated>%s</updated>\n' % format_iso8601(self.updated)
00166         if self.url:
00167             yield u'  <link href="%s" />\n' % escape(self.url, True)
00168         if self.feed_url:
00169             yield u'  <link href="%s" rel="self" />\n' % \
00170                 escape(self.feed_url, True)
00171         for link in self.links:
00172             yield u'  <link %s/>\n' % ''.join('%s="%s" ' % \
00173                 (k, escape(link[k], True)) for k in link)
00174         for author in self.author:
00175             yield u'  <author>\n'
00176             yield u'    <name>%s</name>\n' % escape(author['name'])
00177             if 'uri' in author:
00178                 yield u'    <uri>%s</uri>\n' % escape(author['uri'])
00179             if 'email' in author:
00180                 yield '    <email>%s</email>\n' % escape(author['email'])
00181             yield '  </author>\n'
00182         if self.subtitle:
00183             yield '  ' + _make_text_block('subtitle', self.subtitle,
00184                                           self.subtitle_type)
00185         if self.icon:
00186             yield u'  <icon>%s</icon>\n' % escape(self.icon)
00187         if self.logo:
00188             yield u'  <logo>%s</logo>\n' % escape(self.logo)
00189         if self.rights:
00190             yield '  ' + _make_text_block('rights', self.rights,
00191                                           self.rights_type)
00192         generator_name, generator_url, generator_version = self.generator
00193         if generator_name or generator_url or generator_version:
00194             tmp = [u'  <generator']
00195             if generator_url:
00196                 tmp.append(u' uri="%s"' % escape(generator_url, True))
00197             if generator_version:
00198                 tmp.append(u' version="%s"' % escape(generator_version, True))
00199             tmp.append(u'>%s</generator>\n' % escape(generator_name))
00200             yield u''.join(tmp)
00201         for entry in self.entries:
00202             for line in entry.generate():
00203                 yield u'  ' + line
00204         yield u'</feed>\n'
00205 
00206     def to_string(self):
00207         """Convert the feed into a string."""
00208         return u''.join(self.generate())
00209 
00210     def get_response(self):
00211         """Return a response object for the feed."""
00212         return BaseResponse(self.to_string(), mimetype='application/atom+xml')
00213 
00214     def __call__(self, environ, start_response):
00215         """Use the class as WSGI response object."""
00216         return self.get_response()(environ, start_response)
00217 
00218     def __unicode__(self):
00219         return self.to_string()
00220 
00221     def __str__(self):
00222         return self.to_string().encode('utf-8')
00223 
00224 
00225 class FeedEntry(object):
00226     """Represents a single entry in a feed.
00227 
00228     :param title: the title of the entry. Required.
00229     :param title_type: the type attribute for the title element.  One of
00230                        ``'html'``, ``'text'`` or ``'xhtml'``.
00231     :param content: the content of the entry.
00232     :param content_type: the type attribute for the content element.  One
00233                          of ``'html'``, ``'text'`` or ``'xhtml'``.
00234     :param summary: a summary of the entry's content.
00235     :param summary_type: the type attribute for the summary element.  One
00236                          of ``'html'``, ``'text'`` or ``'xhtml'``.
00237     :param url: the url for the entry.
00238     :param id: a globally unique id for the entry.  Must be an URI.  If
00239                not present the URL is used, but one of both is required.
00240     :param updated: the time the entry was modified the last time.  Must
00241                     be a :class:`datetime.datetime` object. Required.
00242     :param author: the author of the feed.  Must be either a string (the
00243                    name) or a dict with name (required) and uri or
00244                    email (both optional).  Can be a list of (may be
00245                    mixed, too) strings and dicts, too, if there are
00246                    multiple authors. Required if not every entry has an
00247                    author element.
00248     :param published: the time the entry was initially published.  Must
00249                       be a :class:`datetime.datetime` object.
00250     :param rights: copyright information for the entry.
00251     :param rights_type: the type attribute for the rights element.  One of
00252                         ``'html'``, ``'text'`` or ``'xhtml'``.  Default is
00253                         ``'text'``.
00254     :param links: additional links.  Must be a list of dictionaries with
00255                   href (required) and rel, type, hreflang, title, length
00256                   (all optional)
00257     :param xml_base: The xml base (url) for this feed item.  If not provided
00258                      it will default to the item url.
00259 
00260     For more information on the elements see
00261     http://www.atomenabled.org/developers/syndication/
00262 
00263     Everywhere where a list is demanded, any iterable can be used.
00264     """
00265 
00266     def __init__(self, title=None, content=None, feed_url=None, **kwargs):
00267         self.title = title
00268         self.title_type = kwargs.get('title_type', 'text')
00269         self.content = content
00270         self.content_type = kwargs.get('content_type', 'html')
00271         self.url = kwargs.get('url')
00272         self.id = kwargs.get('id', self.url)
00273         self.updated = kwargs.get('updated')
00274         self.summary = kwargs.get('summary')
00275         self.summary_type = kwargs.get('summary_type', 'html')
00276         self.author = kwargs.get('author')
00277         self.published = kwargs.get('published')
00278         self.rights = kwargs.get('rights')
00279         self.links = kwargs.get('links', [])
00280         self.xml_base = kwargs.get('xml_base', feed_url)
00281 
00282         if not hasattr(self.author, '__iter__') \
00283            or isinstance(self.author, (basestring, dict)):
00284             self.author = [self.author]
00285         for i, author in enumerate(self.author):
00286             if not isinstance(author, dict):
00287                 self.author[i] = {'name': author}
00288 
00289         if not self.title:
00290             raise ValueError('title is required')
00291         if not self.id:
00292             raise ValueError('id is required')
00293         if not self.updated:
00294             raise ValueError('updated is required')
00295 
00296     def __repr__(self):
00297         return '<%s %r>' % (
00298             self.__class__.__name__,
00299             self.title
00300         )
00301 
00302     def generate(self):
00303         """Yields pieces of ATOM XML."""
00304         base = ''
00305         if self.xml_base:
00306             base = ' xml:base="%s"' % escape(self.xml_base, True)
00307         yield u'<entry%s>\n' % base
00308         yield u'  ' + _make_text_block('title', self.title, self.title_type)
00309         yield u'  <id>%s</id>\n' % escape(self.id)
00310         yield u'  <updated>%s</updated>\n' % format_iso8601(self.updated)
00311         if self.published:
00312             yield u'  <published>%s</published>\n' % \
00313                   format_iso8601(self.published)
00314         if self.url:
00315             yield u'  <link href="%s" />\n' % escape(self.url)
00316         for author in self.author:
00317             yield u'  <author>\n'
00318             yield u'    <name>%s</name>\n' % escape(author['name'])
00319             if 'uri' in author:
00320                 yield u'    <uri>%s</uri>\n' % escape(author['uri'])
00321             if 'email' in author:
00322                 yield u'    <email>%s</email>\n' % escape(author['email'])
00323             yield u'  </author>\n'
00324         for link in self.links:
00325             yield u'  <link %s/>\n' % ''.join('%s="%s" ' % \
00326                 (k, escape(link[k], True)) for k in link)
00327         if self.summary:
00328             yield u'  ' + _make_text_block('summary', self.summary,
00329                                            self.summary_type)
00330         if self.content:
00331             yield u'  ' + _make_text_block('content', self.content,
00332                                            self.content_type)
00333         yield u'</entry>\n'
00334 
00335     def to_string(self):
00336         """Convert the feed item into a unicode object."""
00337         return u''.join(self.generate())
00338 
00339     def __unicode__(self):
00340         return self.to_string()
00341 
00342     def __str__(self):
00343         return self.to_string().encode('utf-8')