# -*- 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
%s>\n' % (
name,
XHTML_NAMESPACE,
content,
name,
)
if not content_type:
return u"<%s>%s%s>\n" % (name, escape(content), name)
return u'<%s type="%s">%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()