fuzzyworld

fuzzyworld-tools / files / server / atom.py

atom.py

Blame: the commit that last changed each line · file · history

0e603cf7 2026-10-31 Pavel Safronov1"""Atom 1.0 feeds, standard library only.
2
3 atom_feed(title, url, updated, entries, rights, author=None, feed_url=None, subtitle=None) -> str
4
5Used by blog.py for the blog's /feed/. Each entry is a dict:
6 title, url, updated required (`updated` is a datetime, a date or an RFC 3339 string)
7 published, content, summary, id, author optional (`content` is HTML)
8The XML is built with ElementTree, so every text is escaped by the library.
9"""
10
11import datetime
12import re
13import xml.etree.ElementTree as ET
14
15NS = "http://www.w3.org/2005/Atom"
16ET.register_namespace("", NS)
17
18# Characters XML 1.0 cannot carry at all (a stray form feed in a post would make the feed unparseable).
19_ILLEGAL = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f]")
20
21
22def rfc3339(value):
23 """2018-03-02T00:00:00Z. Naive datetimes and dates are written as UTC."""
24 if isinstance(value, str):
25 return value
26 if isinstance(value, datetime.datetime):
27 if value.tzinfo:
28 value = value.astimezone(datetime.timezone.utc).replace(tzinfo=None)
29 return value.strftime("%Y-%m-%dT%H:%M:%SZ")
30 return datetime.datetime(value.year, value.month, value.day).strftime("%Y-%m-%dT%H:%M:%SZ")
31
32
33def _el(parent, tag, text=None, **attrs):
34 e = ET.SubElement(parent, f"{{{NS}}}{tag}", {k: v for k, v in attrs.items() if v is not None})
35 if text is not None:
36 e.text = _ILLEGAL.sub("", text)
37 return e
38
39
40def _author(parent, name):
41 _el(_el(parent, "author"), "name", name)
42
43
44def atom_feed(title, url, updated, entries, rights="", author=None, feed_url=None, subtitle=None):
45 root = ET.Element(f"{{{NS}}}feed")
46 _el(root, "title", title)
47 if subtitle:
48 _el(root, "subtitle", subtitle)
49 _el(root, "id", url)
50 _el(root, "updated", rfc3339(updated))
51 _el(root, "link", rel="alternate", type="text/html", href=url)
52 _el(root, "link", rel="self", type="application/atom+xml", href=feed_url or url)
53 if rights:
54 _el(root, "rights", rights)
55 if author:
56 _author(root, author)
57 for e in entries:
58 item = _el(root, "entry")
59 _el(item, "title", e["title"])
60 _el(item, "id", e.get("id") or e["url"])
61 _el(item, "link", rel="alternate", type="text/html", href=e["url"])
62 _el(item, "updated", rfc3339(e["updated"]))
63 if e.get("published"):
64 _el(item, "published", rfc3339(e["published"]))
65 if e.get("author"):
66 _author(item, e["author"])
67 if e.get("summary"):
68 _el(item, "summary", e["summary"], type="text")
69 if e.get("content"):
70 _el(item, "content", e["content"], type="html")
71 ET.indent(root)
72 return '<?xml version="1.0" encoding="utf-8"?>\n' + ET.tostring(root, encoding="unicode") + "\n"