# -*- coding: utf-8 -*- """ werkzeug.contrib.atom ~~~~~~~~~~~~~~~~~~~~~ This module provides a class called :class:`AtomFeed` which can be used to generate feeds in the Atom syndication format (see :rfc:`4287`). Example:: def atom_feed(request): feed = AtomFeed("My Blog", feed_url=request.url, url=request.host_url, subtitle="My example blog for a feed test.") for post in Post.query.limit(10).all(): feed.add(post.title, post.body, content_type='html', author=post.author, url=post.url, id=post.uid, updated=post.last_update, published=post.pub_date) return feed.get_response() :copyright: 2007 Pallets :license: BSD-3-Clause """ import warnings from datetime import datetime from .._compat import implements_to_string from .._compat import string_types from ..utils import escape from ..wrappers import BaseResponse warnings.warn( "'werkzeug.contrib.atom' is deprecated as of version 0.15 and will" " be removed in version 1.0.", DeprecationWarning, stacklevel=2, ) XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml" def _make_text_block(name, content, content_type=None): """Helper function for the builder that creates an XML text block.""" if content_type == "xhtml": return u'<%s type="xhtml">
%s
\n' % ( name, XHTML_NAMESPACE, content, name, ) if not content_type: return u"<%s>%s\n" % (name, escape(content), name) return u'<%s type="%s">%s\n' % (name, content_type, escape(content), name) def format_iso8601(obj): """Format a datetime object for iso8601""" iso8601 = obj.isoformat() if obj.tzinfo: return iso8601 return iso8601 + "Z" @implements_to_string class AtomFeed(object): """A helper class that creates Atom feeds. :param title: the title of the feed. Required. :param title_type: the type attribute for the title element. One of ``'html'``, ``'text'`` or ``'xhtml'``. :param url: the url for the feed (not the url *of* the feed) :param id: a globally unique id for the feed. Must be an URI. If not present the `feed_url` is used, but one of both is required. :param updated: the time the feed was modified the last time. Must be a :class:`datetime.datetime` object. If not present the latest entry's `updated` is used. Treated as UTC if naive datetime. :param feed_url: the URL to the feed. Should be the URL that was requested. :param author: the author of the feed. Must be either a string (the name) or a dict with name (required) and uri or email (both optional). Can be a list of (may be mixed, too) strings and dicts, too, if there are multiple authors. Required if not every entry has an author element. :param icon: an icon for the feed. :param logo: a logo for the feed. :param rights: copyright information for the feed. :param rights_type: the type attribute for the rights element. One of ``'html'``, ``'text'`` or ``'xhtml'``. Default is ``'text'``. :param subtitle: a short description of the feed. :param subtitle_type: the type attribute for the subtitle element. One of ``'text'``, ``'html'``, ``'text'`` or ``'xhtml'``. Default is ``'text'``. :param links: additional links. Must be a list of dictionaries with href (required) and rel, type, hreflang, title, length (all optional) :param generator: the software that generated this feed. This must be a tuple in the form ``(name, url, version)``. If you don't want to specify one of them, set the item to `None`. :param entries: a list with the entries for the feed. Entries can also be added later with :meth:`add`. For more information on the elements see http://www.atomenabled.org/developers/syndication/ Everywhere where a list is demanded, any iterable can be used. """ default_generator = ("Werkzeug", None, None) def __init__(self, title=None, entries=None, **kwargs): self.title = title self.title_type = kwargs.get("title_type", "text") self.url = kwargs.get("url") self.feed_url = kwargs.get("feed_url", self.url) self.id = kwargs.get("id", self.feed_url) self.updated = kwargs.get("updated") self.author = kwargs.get("author", ()) self.icon = kwargs.get("icon") self.logo = kwargs.get("logo") self.rights = kwargs.get("rights") self.rights_type = kwargs.get("rights_type") self.subtitle = kwargs.get("subtitle") self.subtitle_type = kwargs.get("subtitle_type", "text") self.generator = kwargs.get("generator") if self.generator is None: self.generator = self.default_generator self.links = kwargs.get("links", []) self.entries = list(entries) if entries else [] if not hasattr(self.author, "__iter__") or isinstance( self.author, string_types + (dict,) ): self.author = [self.author] for i, author in enumerate(self.author): if not isinstance(author, dict): self.author[i] = {"name": author} if not self.title: raise ValueError("title is required") if not self.id: raise ValueError("id is required") for author in self.author: if "name" not in author: raise TypeError("author must contain at least a name") def add(self, *args, **kwargs): """Add a new entry to the feed. This function can either be called with a :class:`FeedEntry` or some keyword and positional arguments that are forwarded to the :class:`FeedEntry` constructor. """ if len(args) == 1 and not kwargs and isinstance(args[0], FeedEntry): self.entries.append(args[0]) else: kwargs["feed_url"] = self.feed_url self.entries.append(FeedEntry(*args, **kwargs)) def __repr__(self): return "<%s %r (%d entries)>" % ( self.__class__.__name__, self.title, len(self.entries), ) def generate(self): """Return a generator that yields pieces of XML.""" # atom demands either an author element in every entry or a global one if not self.author: if any(not e.author for e in self.entries): self.author = ({"name": "Unknown author"},) if not self.updated: dates = sorted([entry.updated for entry in self.entries]) self.updated = dates[-1] if dates else datetime.utcnow() yield u'\n' yield u'\n' yield " " + _make_text_block("title", self.title, self.title_type) yield u" %s\n" % escape(self.id) yield u" %s\n" % format_iso8601(self.updated) if self.url: yield u' \n' % escape(self.url) if self.feed_url: yield u' \n' % escape(self.feed_url) for link in self.links: yield u" \n" % "".join( '%s="%s" ' % (k, escape(link[k])) for k in link ) for author in self.author: yield u" \n" yield u" %s\n" % escape(author["name"]) if "uri" in author: yield u" %s\n" % escape(author["uri"]) if "email" in author: yield " %s\n" % escape(author["email"]) yield " \n" if self.subtitle: yield " " + _make_text_block("subtitle", self.subtitle, self.subtitle_type) if self.icon: yield u" %s\n" % escape(self.icon) if self.logo: yield u" %s\n" % escape(self.logo) if self.rights: yield " " + _make_text_block("rights", self.rights, self.rights_type) generator_name, generator_url, generator_version = self.generator if generator_name or generator_url or generator_version: tmp = [u" %s\n" % escape(generator_name)) yield u"".join(tmp) for entry in self.entries: for line in entry.generate(): yield u" " + line yield u"\n" def to_string(self): """Convert the feed into a string.""" return u"".join(self.generate()) def get_response(self): """Return a response object for the feed.""" return BaseResponse(self.to_string(), mimetype="application/atom+xml") def __call__(self, environ, start_response): """Use the class as WSGI response object.""" return self.get_response()(environ, start_response) def __str__(self): return self.to_string() @implements_to_string class FeedEntry(object): """Represents a single entry in a feed. :param title: the title of the entry. Required. :param title_type: the type attribute for the title element. One of ``'html'``, ``'text'`` or ``'xhtml'``. :param content: the content of the entry. :param content_type: the type attribute for the content element. One of ``'html'``, ``'text'`` or ``'xhtml'``. :param summary: a summary of the entry's content. :param summary_type: the type attribute for the summary element. One of ``'html'``, ``'text'`` or ``'xhtml'``. :param url: the url for the entry. :param id: a globally unique id for the entry. Must be an URI. If not present the URL is used, but one of both is required. :param updated: the time the entry was modified the last time. Must be a :class:`datetime.datetime` object. Treated as UTC if naive datetime. Required. :param author: the author of the entry. Must be either a string (the name) or a dict with name (required) and uri or email (both optional). Can be a list of (may be mixed, too) strings and dicts, too, if there are multiple authors. Required if the feed does not have an author element. :param published: the time the entry was initially published. Must be a :class:`datetime.datetime` object. Treated as UTC if naive datetime. :param rights: copyright information for the entry. :param rights_type: the type attribute for the rights element. One of ``'html'``, ``'text'`` or ``'xhtml'``. Default is ``'text'``. :param links: additional links. Must be a list of dictionaries with href (required) and rel, type, hreflang, title, length (all optional) :param categories: categories for the entry. Must be a list of dictionaries with term (required), scheme and label (all optional) :param xml_base: The xml base (url) for this feed item. If not provided it will default to the item url. For more information on the elements see http://www.atomenabled.org/developers/syndication/ Everywhere where a list is demanded, any iterable can be used. """ def __init__(self, title=None, content=None, feed_url=None, **kwargs): self.title = title self.title_type = kwargs.get("title_type", "text") self.content = content self.content_type = kwargs.get("content_type", "html") self.url = kwargs.get("url") self.id = kwargs.get("id", self.url) self.updated = kwargs.get("updated") self.summary = kwargs.get("summary") self.summary_type = kwargs.get("summary_type", "html") self.author = kwargs.get("author", ()) self.published = kwargs.get("published") self.rights = kwargs.get("rights") self.links = kwargs.get("links", []) self.categories = kwargs.get("categories", []) self.xml_base = kwargs.get("xml_base", feed_url) if not hasattr(self.author, "__iter__") or isinstance( self.author, string_types + (dict,) ): self.author = [self.author] for i, author in enumerate(self.author): if not isinstance(author, dict): self.author[i] = {"name": author} if not self.title: raise ValueError("title is required") if not self.id: raise ValueError("id is required") if not self.updated: raise ValueError("updated is required") def __repr__(self): return "<%s %r>" % (self.__class__.__name__, self.title) def generate(self): """Yields pieces of ATOM XML.""" base = "" if self.xml_base: base = ' xml:base="%s"' % escape(self.xml_base) yield u"\n" % base yield u" " + _make_text_block("title", self.title, self.title_type) yield u" %s\n" % escape(self.id) yield u" %s\n" % format_iso8601(self.updated) if self.published: yield u" %s\n" % format_iso8601(self.published) if self.url: yield u' \n' % escape(self.url) for author in self.author: yield u" \n" yield u" %s\n" % escape(author["name"]) if "uri" in author: yield u" %s\n" % escape(author["uri"]) if "email" in author: yield u" %s\n" % escape(author["email"]) yield u" \n" for link in self.links: yield u" \n" % "".join( '%s="%s" ' % (k, escape(link[k])) for k in link ) for category in self.categories: yield u" \n" % "".join( '%s="%s" ' % (k, escape(category[k])) for k in category ) if self.summary: yield u" " + _make_text_block("summary", self.summary, self.summary_type) if self.content: yield u" " + _make_text_block("content", self.content, self.content_type) yield u"\n" def to_string(self): """Convert the feed item into a unicode object.""" return u"".join(self.generate()) def __str__(self): return self.to_string()