atom.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. # -*- coding: utf-8 -*-
  2. """
  3. werkzeug.contrib.atom
  4. ~~~~~~~~~~~~~~~~~~~~~
  5. This module provides a class called :class:`AtomFeed` which can be
  6. used to generate feeds in the Atom syndication format (see :rfc:`4287`).
  7. Example::
  8. def atom_feed(request):
  9. feed = AtomFeed("My Blog", feed_url=request.url,
  10. url=request.host_url,
  11. subtitle="My example blog for a feed test.")
  12. for post in Post.query.limit(10).all():
  13. feed.add(post.title, post.body, content_type='html',
  14. author=post.author, url=post.url, id=post.uid,
  15. updated=post.last_update, published=post.pub_date)
  16. return feed.get_response()
  17. :copyright: 2007 Pallets
  18. :license: BSD-3-Clause
  19. """
  20. import warnings
  21. from datetime import datetime
  22. from .._compat import implements_to_string
  23. from .._compat import string_types
  24. from ..utils import escape
  25. from ..wrappers import BaseResponse
  26. warnings.warn(
  27. "'werkzeug.contrib.atom' is deprecated as of version 0.15 and will"
  28. " be removed in version 1.0.",
  29. DeprecationWarning,
  30. stacklevel=2,
  31. )
  32. XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
  33. def _make_text_block(name, content, content_type=None):
  34. """Helper function for the builder that creates an XML text block."""
  35. if content_type == "xhtml":
  36. return u'<%s type="xhtml"><div xmlns="%s">%s</div></%s>\n' % (
  37. name,
  38. XHTML_NAMESPACE,
  39. content,
  40. name,
  41. )
  42. if not content_type:
  43. return u"<%s>%s</%s>\n" % (name, escape(content), name)
  44. return u'<%s type="%s">%s</%s>\n' % (name, content_type, escape(content), name)
  45. def format_iso8601(obj):
  46. """Format a datetime object for iso8601"""
  47. iso8601 = obj.isoformat()
  48. if obj.tzinfo:
  49. return iso8601
  50. return iso8601 + "Z"
  51. @implements_to_string
  52. class AtomFeed(object):
  53. """A helper class that creates Atom feeds.
  54. :param title: the title of the feed. Required.
  55. :param title_type: the type attribute for the title element. One of
  56. ``'html'``, ``'text'`` or ``'xhtml'``.
  57. :param url: the url for the feed (not the url *of* the feed)
  58. :param id: a globally unique id for the feed. Must be an URI. If
  59. not present the `feed_url` is used, but one of both is
  60. required.
  61. :param updated: the time the feed was modified the last time. Must
  62. be a :class:`datetime.datetime` object. If not
  63. present the latest entry's `updated` is used.
  64. Treated as UTC if naive datetime.
  65. :param feed_url: the URL to the feed. Should be the URL that was
  66. requested.
  67. :param author: the author of the feed. Must be either a string (the
  68. name) or a dict with name (required) and uri or
  69. email (both optional). Can be a list of (may be
  70. mixed, too) strings and dicts, too, if there are
  71. multiple authors. Required if not every entry has an
  72. author element.
  73. :param icon: an icon for the feed.
  74. :param logo: a logo for the feed.
  75. :param rights: copyright information for the feed.
  76. :param rights_type: the type attribute for the rights element. One of
  77. ``'html'``, ``'text'`` or ``'xhtml'``. Default is
  78. ``'text'``.
  79. :param subtitle: a short description of the feed.
  80. :param subtitle_type: the type attribute for the subtitle element.
  81. One of ``'text'``, ``'html'``, ``'text'``
  82. or ``'xhtml'``. Default is ``'text'``.
  83. :param links: additional links. Must be a list of dictionaries with
  84. href (required) and rel, type, hreflang, title, length
  85. (all optional)
  86. :param generator: the software that generated this feed. This must be
  87. a tuple in the form ``(name, url, version)``. If
  88. you don't want to specify one of them, set the item
  89. to `None`.
  90. :param entries: a list with the entries for the feed. Entries can also
  91. be added later with :meth:`add`.
  92. For more information on the elements see
  93. http://www.atomenabled.org/developers/syndication/
  94. Everywhere where a list is demanded, any iterable can be used.
  95. """
  96. default_generator = ("Werkzeug", None, None)
  97. def __init__(self, title=None, entries=None, **kwargs):
  98. self.title = title
  99. self.title_type = kwargs.get("title_type", "text")
  100. self.url = kwargs.get("url")
  101. self.feed_url = kwargs.get("feed_url", self.url)
  102. self.id = kwargs.get("id", self.feed_url)
  103. self.updated = kwargs.get("updated")
  104. self.author = kwargs.get("author", ())
  105. self.icon = kwargs.get("icon")
  106. self.logo = kwargs.get("logo")
  107. self.rights = kwargs.get("rights")
  108. self.rights_type = kwargs.get("rights_type")
  109. self.subtitle = kwargs.get("subtitle")
  110. self.subtitle_type = kwargs.get("subtitle_type", "text")
  111. self.generator = kwargs.get("generator")
  112. if self.generator is None:
  113. self.generator = self.default_generator
  114. self.links = kwargs.get("links", [])
  115. self.entries = list(entries) if entries else []
  116. if not hasattr(self.author, "__iter__") or isinstance(
  117. self.author, string_types + (dict,)
  118. ):
  119. self.author = [self.author]
  120. for i, author in enumerate(self.author):
  121. if not isinstance(author, dict):
  122. self.author[i] = {"name": author}
  123. if not self.title:
  124. raise ValueError("title is required")
  125. if not self.id:
  126. raise ValueError("id is required")
  127. for author in self.author:
  128. if "name" not in author:
  129. raise TypeError("author must contain at least a name")
  130. def add(self, *args, **kwargs):
  131. """Add a new entry to the feed. This function can either be called
  132. with a :class:`FeedEntry` or some keyword and positional arguments
  133. that are forwarded to the :class:`FeedEntry` constructor.
  134. """
  135. if len(args) == 1 and not kwargs and isinstance(args[0], FeedEntry):
  136. self.entries.append(args[0])
  137. else:
  138. kwargs["feed_url"] = self.feed_url
  139. self.entries.append(FeedEntry(*args, **kwargs))
  140. def __repr__(self):
  141. return "<%s %r (%d entries)>" % (
  142. self.__class__.__name__,
  143. self.title,
  144. len(self.entries),
  145. )
  146. def generate(self):
  147. """Return a generator that yields pieces of XML."""
  148. # atom demands either an author element in every entry or a global one
  149. if not self.author:
  150. if any(not e.author for e in self.entries):
  151. self.author = ({"name": "Unknown author"},)
  152. if not self.updated:
  153. dates = sorted([entry.updated for entry in self.entries])
  154. self.updated = dates[-1] if dates else datetime.utcnow()
  155. yield u'<?xml version="1.0" encoding="utf-8"?>\n'
  156. yield u'<feed xmlns="http://www.w3.org/2005/Atom">\n'
  157. yield " " + _make_text_block("title", self.title, self.title_type)
  158. yield u" <id>%s</id>\n" % escape(self.id)
  159. yield u" <updated>%s</updated>\n" % format_iso8601(self.updated)
  160. if self.url:
  161. yield u' <link href="%s" />\n' % escape(self.url)
  162. if self.feed_url:
  163. yield u' <link href="%s" rel="self" />\n' % escape(self.feed_url)
  164. for link in self.links:
  165. yield u" <link %s/>\n" % "".join(
  166. '%s="%s" ' % (k, escape(link[k])) for k in link
  167. )
  168. for author in self.author:
  169. yield u" <author>\n"
  170. yield u" <name>%s</name>\n" % escape(author["name"])
  171. if "uri" in author:
  172. yield u" <uri>%s</uri>\n" % escape(author["uri"])
  173. if "email" in author:
  174. yield " <email>%s</email>\n" % escape(author["email"])
  175. yield " </author>\n"
  176. if self.subtitle:
  177. yield " " + _make_text_block("subtitle", self.subtitle, self.subtitle_type)
  178. if self.icon:
  179. yield u" <icon>%s</icon>\n" % escape(self.icon)
  180. if self.logo:
  181. yield u" <logo>%s</logo>\n" % escape(self.logo)
  182. if self.rights:
  183. yield " " + _make_text_block("rights", self.rights, self.rights_type)
  184. generator_name, generator_url, generator_version = self.generator
  185. if generator_name or generator_url or generator_version:
  186. tmp = [u" <generator"]
  187. if generator_url:
  188. tmp.append(u' uri="%s"' % escape(generator_url))
  189. if generator_version:
  190. tmp.append(u' version="%s"' % escape(generator_version))
  191. tmp.append(u">%s</generator>\n" % escape(generator_name))
  192. yield u"".join(tmp)
  193. for entry in self.entries:
  194. for line in entry.generate():
  195. yield u" " + line
  196. yield u"</feed>\n"
  197. def to_string(self):
  198. """Convert the feed into a string."""
  199. return u"".join(self.generate())
  200. def get_response(self):
  201. """Return a response object for the feed."""
  202. return BaseResponse(self.to_string(), mimetype="application/atom+xml")
  203. def __call__(self, environ, start_response):
  204. """Use the class as WSGI response object."""
  205. return self.get_response()(environ, start_response)
  206. def __str__(self):
  207. return self.to_string()
  208. @implements_to_string
  209. class FeedEntry(object):
  210. """Represents a single entry in a feed.
  211. :param title: the title of the entry. Required.
  212. :param title_type: the type attribute for the title element. One of
  213. ``'html'``, ``'text'`` or ``'xhtml'``.
  214. :param content: the content of the entry.
  215. :param content_type: the type attribute for the content element. One
  216. of ``'html'``, ``'text'`` or ``'xhtml'``.
  217. :param summary: a summary of the entry's content.
  218. :param summary_type: the type attribute for the summary element. One
  219. of ``'html'``, ``'text'`` or ``'xhtml'``.
  220. :param url: the url for the entry.
  221. :param id: a globally unique id for the entry. Must be an URI. If
  222. not present the URL is used, but one of both is required.
  223. :param updated: the time the entry was modified the last time. Must
  224. be a :class:`datetime.datetime` object. Treated as
  225. UTC if naive datetime. Required.
  226. :param author: the author of the entry. Must be either a string (the
  227. name) or a dict with name (required) and uri or
  228. email (both optional). Can be a list of (may be
  229. mixed, too) strings and dicts, too, if there are
  230. multiple authors. Required if the feed does not have an
  231. author element.
  232. :param published: the time the entry was initially published. Must
  233. be a :class:`datetime.datetime` object. Treated as
  234. UTC if naive datetime.
  235. :param rights: copyright information for the entry.
  236. :param rights_type: the type attribute for the rights element. One of
  237. ``'html'``, ``'text'`` or ``'xhtml'``. Default is
  238. ``'text'``.
  239. :param links: additional links. Must be a list of dictionaries with
  240. href (required) and rel, type, hreflang, title, length
  241. (all optional)
  242. :param categories: categories for the entry. Must be a list of dictionaries
  243. with term (required), scheme and label (all optional)
  244. :param xml_base: The xml base (url) for this feed item. If not provided
  245. it will default to the item url.
  246. For more information on the elements see
  247. http://www.atomenabled.org/developers/syndication/
  248. Everywhere where a list is demanded, any iterable can be used.
  249. """
  250. def __init__(self, title=None, content=None, feed_url=None, **kwargs):
  251. self.title = title
  252. self.title_type = kwargs.get("title_type", "text")
  253. self.content = content
  254. self.content_type = kwargs.get("content_type", "html")
  255. self.url = kwargs.get("url")
  256. self.id = kwargs.get("id", self.url)
  257. self.updated = kwargs.get("updated")
  258. self.summary = kwargs.get("summary")
  259. self.summary_type = kwargs.get("summary_type", "html")
  260. self.author = kwargs.get("author", ())
  261. self.published = kwargs.get("published")
  262. self.rights = kwargs.get("rights")
  263. self.links = kwargs.get("links", [])
  264. self.categories = kwargs.get("categories", [])
  265. self.xml_base = kwargs.get("xml_base", feed_url)
  266. if not hasattr(self.author, "__iter__") or isinstance(
  267. self.author, string_types + (dict,)
  268. ):
  269. self.author = [self.author]
  270. for i, author in enumerate(self.author):
  271. if not isinstance(author, dict):
  272. self.author[i] = {"name": author}
  273. if not self.title:
  274. raise ValueError("title is required")
  275. if not self.id:
  276. raise ValueError("id is required")
  277. if not self.updated:
  278. raise ValueError("updated is required")
  279. def __repr__(self):
  280. return "<%s %r>" % (self.__class__.__name__, self.title)
  281. def generate(self):
  282. """Yields pieces of ATOM XML."""
  283. base = ""
  284. if self.xml_base:
  285. base = ' xml:base="%s"' % escape(self.xml_base)
  286. yield u"<entry%s>\n" % base
  287. yield u" " + _make_text_block("title", self.title, self.title_type)
  288. yield u" <id>%s</id>\n" % escape(self.id)
  289. yield u" <updated>%s</updated>\n" % format_iso8601(self.updated)
  290. if self.published:
  291. yield u" <published>%s</published>\n" % format_iso8601(self.published)
  292. if self.url:
  293. yield u' <link href="%s" />\n' % escape(self.url)
  294. for author in self.author:
  295. yield u" <author>\n"
  296. yield u" <name>%s</name>\n" % escape(author["name"])
  297. if "uri" in author:
  298. yield u" <uri>%s</uri>\n" % escape(author["uri"])
  299. if "email" in author:
  300. yield u" <email>%s</email>\n" % escape(author["email"])
  301. yield u" </author>\n"
  302. for link in self.links:
  303. yield u" <link %s/>\n" % "".join(
  304. '%s="%s" ' % (k, escape(link[k])) for k in link
  305. )
  306. for category in self.categories:
  307. yield u" <category %s/>\n" % "".join(
  308. '%s="%s" ' % (k, escape(category[k])) for k in category
  309. )
  310. if self.summary:
  311. yield u" " + _make_text_block("summary", self.summary, self.summary_type)
  312. if self.content:
  313. yield u" " + _make_text_block("content", self.content, self.content_type)
  314. yield u"</entry>\n"
  315. def to_string(self):
  316. """Convert the feed item into a unicode object."""
  317. return u"".join(self.generate())
  318. def __str__(self):
  319. return self.to_string()