Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/utils/feedgenerator.py: 26%
204 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
1"""
2Syndication feed generation library -- used for generating RSS, etc.
4Sample usage:
6>>> from django.utils import feedgenerator
7>>> feed = feedgenerator.Rss201rev2Feed(
8... title="Poynter E-Media Tidbits",
9... link="http://www.poynter.org/column.asp?id=31",
10... description="A group blog by the sharpest minds in online journalism.",
11... language="en",
12... )
13>>> feed.add_item(
14... title="Hello",
15... link="http://www.holovaty.com/test/",
16... description="Testing."
17... )
18>>> with open('test.rss', 'w') as fp:
19... feed.write(fp, 'utf-8')
21For definitions of the different versions of RSS, see:
22https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss
23"""
24import datetime
25import email
26from io import StringIO
27from urllib.parse import urlparse
29from django.utils.encoding import iri_to_uri
30from django.utils.xmlutils import SimplerXMLGenerator
33def rfc2822_date(date):
34 if not isinstance(date, datetime.datetime):
35 date = datetime.datetime.combine(date, datetime.time())
36 return email.utils.format_datetime(date)
39def rfc3339_date(date):
40 if not isinstance(date, datetime.datetime):
41 date = datetime.datetime.combine(date, datetime.time())
42 return date.isoformat() + ("Z" if date.utcoffset() is None else "")
45def get_tag_uri(url, date):
46 """
47 Create a TagURI.
49 See
50 https://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
51 """
52 bits = urlparse(url)
53 d = ""
54 if date is not None:
55 d = ",%s" % date.strftime("%Y-%m-%d")
56 return "tag:%s%s:%s/%s" % (bits.hostname, d, bits.path, bits.fragment)
59class SyndicationFeed:
60 "Base class for all syndication feeds. Subclasses should provide write()"
62 def __init__(
63 self,
64 title,
65 link,
66 description,
67 language=None,
68 author_email=None,
69 author_name=None,
70 author_link=None,
71 subtitle=None,
72 categories=None,
73 feed_url=None,
74 feed_copyright=None,
75 feed_guid=None,
76 ttl=None,
77 **kwargs,
78 ):
79 def to_str(s):
80 return str(s) if s is not None else s
82 categories = categories and [str(c) for c in categories]
83 self.feed = {
84 "title": to_str(title),
85 "link": iri_to_uri(link),
86 "description": to_str(description),
87 "language": to_str(language),
88 "author_email": to_str(author_email),
89 "author_name": to_str(author_name),
90 "author_link": iri_to_uri(author_link),
91 "subtitle": to_str(subtitle),
92 "categories": categories or (),
93 "feed_url": iri_to_uri(feed_url),
94 "feed_copyright": to_str(feed_copyright),
95 "id": feed_guid or link,
96 "ttl": to_str(ttl),
97 **kwargs,
98 }
99 self.items = []
101 def add_item(
102 self,
103 title,
104 link,
105 description,
106 author_email=None,
107 author_name=None,
108 author_link=None,
109 pubdate=None,
110 comments=None,
111 unique_id=None,
112 unique_id_is_permalink=None,
113 categories=(),
114 item_copyright=None,
115 ttl=None,
116 updateddate=None,
117 enclosures=None,
118 **kwargs,
119 ):
120 """
121 Add an item to the feed. All args are expected to be strings except
122 pubdate and updateddate, which are datetime.datetime objects, and
123 enclosures, which is an iterable of instances of the Enclosure class.
124 """
126 def to_str(s):
127 return str(s) if s is not None else s
129 categories = categories and [to_str(c) for c in categories]
130 self.items.append(
131 {
132 "title": to_str(title),
133 "link": iri_to_uri(link),
134 "description": to_str(description),
135 "author_email": to_str(author_email),
136 "author_name": to_str(author_name),
137 "author_link": iri_to_uri(author_link),
138 "pubdate": pubdate,
139 "updateddate": updateddate,
140 "comments": to_str(comments),
141 "unique_id": to_str(unique_id),
142 "unique_id_is_permalink": unique_id_is_permalink,
143 "enclosures": enclosures or (),
144 "categories": categories or (),
145 "item_copyright": to_str(item_copyright),
146 "ttl": to_str(ttl),
147 **kwargs,
148 }
149 )
151 def num_items(self):
152 return len(self.items)
154 def root_attributes(self):
155 """
156 Return extra attributes to place on the root (i.e. feed/channel) element.
157 Called from write().
158 """
159 return {}
161 def add_root_elements(self, handler):
162 """
163 Add elements in the root (i.e. feed/channel) element. Called
164 from write().
165 """
166 pass
168 def item_attributes(self, item):
169 """
170 Return extra attributes to place on each item (i.e. item/entry) element.
171 """
172 return {}
174 def add_item_elements(self, handler, item):
175 """
176 Add elements on each item (i.e. item/entry) element.
177 """
178 pass
180 def write(self, outfile, encoding):
181 """
182 Output the feed in the given encoding to outfile, which is a file-like
183 object. Subclasses should override this.
184 """
185 raise NotImplementedError(
186 "subclasses of SyndicationFeed must provide a write() method"
187 )
189 def writeString(self, encoding):
190 """
191 Return the feed in the given encoding as a string.
192 """
193 s = StringIO()
194 self.write(s, encoding)
195 return s.getvalue()
197 def latest_post_date(self):
198 """
199 Return the latest item's pubdate or updateddate. If no items
200 have either of these attributes this return the current UTC date/time.
201 """
202 latest_date = None
203 date_keys = ("updateddate", "pubdate")
205 for item in self.items:
206 for date_key in date_keys:
207 item_date = item.get(date_key)
208 if item_date:
209 if latest_date is None or item_date > latest_date:
210 latest_date = item_date
212 return latest_date or datetime.datetime.now(tz=datetime.timezone.utc)
215class Enclosure:
216 """An RSS enclosure"""
218 def __init__(self, url, length, mime_type):
219 "All args are expected to be strings"
220 self.length, self.mime_type = length, mime_type
221 self.url = iri_to_uri(url)
224class RssFeed(SyndicationFeed):
225 content_type = "application/rss+xml; charset=utf-8"
227 def write(self, outfile, encoding):
228 handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
229 handler.startDocument()
230 handler.startElement("rss", self.rss_attributes())
231 handler.startElement("channel", self.root_attributes())
232 self.add_root_elements(handler)
233 self.write_items(handler)
234 self.endChannelElement(handler)
235 handler.endElement("rss")
237 def rss_attributes(self):
238 return {
239 "version": self._version,
240 "xmlns:atom": "http://www.w3.org/2005/Atom",
241 }
243 def write_items(self, handler):
244 for item in self.items:
245 handler.startElement("item", self.item_attributes(item))
246 self.add_item_elements(handler, item)
247 handler.endElement("item")
249 def add_root_elements(self, handler):
250 handler.addQuickElement("title", self.feed["title"])
251 handler.addQuickElement("link", self.feed["link"])
252 handler.addQuickElement("description", self.feed["description"])
253 if self.feed["feed_url"] is not None:
254 handler.addQuickElement(
255 "atom:link", None, {"rel": "self", "href": self.feed["feed_url"]}
256 )
257 if self.feed["language"] is not None:
258 handler.addQuickElement("language", self.feed["language"])
259 for cat in self.feed["categories"]:
260 handler.addQuickElement("category", cat)
261 if self.feed["feed_copyright"] is not None:
262 handler.addQuickElement("copyright", self.feed["feed_copyright"])
263 handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date()))
264 if self.feed["ttl"] is not None:
265 handler.addQuickElement("ttl", self.feed["ttl"])
267 def endChannelElement(self, handler):
268 handler.endElement("channel")
271class RssUserland091Feed(RssFeed):
272 _version = "0.91"
274 def add_item_elements(self, handler, item):
275 handler.addQuickElement("title", item["title"])
276 handler.addQuickElement("link", item["link"])
277 if item["description"] is not None:
278 handler.addQuickElement("description", item["description"])
281class Rss201rev2Feed(RssFeed):
282 # Spec: https://cyber.harvard.edu/rss/rss.html
283 _version = "2.0"
285 def add_item_elements(self, handler, item):
286 handler.addQuickElement("title", item["title"])
287 handler.addQuickElement("link", item["link"])
288 if item["description"] is not None:
289 handler.addQuickElement("description", item["description"])
291 # Author information.
292 if item["author_name"] and item["author_email"]:
293 handler.addQuickElement(
294 "author", "%s (%s)" % (item["author_email"], item["author_name"])
295 )
296 elif item["author_email"]:
297 handler.addQuickElement("author", item["author_email"])
298 elif item["author_name"]:
299 handler.addQuickElement(
300 "dc:creator",
301 item["author_name"],
302 {"xmlns:dc": "http://purl.org/dc/elements/1.1/"},
303 )
305 if item["pubdate"] is not None:
306 handler.addQuickElement("pubDate", rfc2822_date(item["pubdate"]))
307 if item["comments"] is not None:
308 handler.addQuickElement("comments", item["comments"])
309 if item["unique_id"] is not None:
310 guid_attrs = {}
311 if isinstance(item.get("unique_id_is_permalink"), bool):
312 guid_attrs["isPermaLink"] = str(item["unique_id_is_permalink"]).lower()
313 handler.addQuickElement("guid", item["unique_id"], guid_attrs)
314 if item["ttl"] is not None:
315 handler.addQuickElement("ttl", item["ttl"])
317 # Enclosure.
318 if item["enclosures"]:
319 enclosures = list(item["enclosures"])
320 if len(enclosures) > 1:
321 raise ValueError(
322 "RSS feed items may only have one enclosure, see "
323 "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
324 )
325 enclosure = enclosures[0]
326 handler.addQuickElement(
327 "enclosure",
328 "",
329 {
330 "url": enclosure.url,
331 "length": enclosure.length,
332 "type": enclosure.mime_type,
333 },
334 )
336 # Categories.
337 for cat in item["categories"]:
338 handler.addQuickElement("category", cat)
341class Atom1Feed(SyndicationFeed):
342 # Spec: https://tools.ietf.org/html/rfc4287
343 content_type = "application/atom+xml; charset=utf-8"
344 ns = "http://www.w3.org/2005/Atom"
346 def write(self, outfile, encoding):
347 handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
348 handler.startDocument()
349 handler.startElement("feed", self.root_attributes())
350 self.add_root_elements(handler)
351 self.write_items(handler)
352 handler.endElement("feed")
354 def root_attributes(self):
355 if self.feed["language"] is not None:
356 return {"xmlns": self.ns, "xml:lang": self.feed["language"]}
357 else:
358 return {"xmlns": self.ns}
360 def add_root_elements(self, handler):
361 handler.addQuickElement("title", self.feed["title"])
362 handler.addQuickElement(
363 "link", "", {"rel": "alternate", "href": self.feed["link"]}
364 )
365 if self.feed["feed_url"] is not None:
366 handler.addQuickElement(
367 "link", "", {"rel": "self", "href": self.feed["feed_url"]}
368 )
369 handler.addQuickElement("id", self.feed["id"])
370 handler.addQuickElement("updated", rfc3339_date(self.latest_post_date()))
371 if self.feed["author_name"] is not None:
372 handler.startElement("author", {})
373 handler.addQuickElement("name", self.feed["author_name"])
374 if self.feed["author_email"] is not None:
375 handler.addQuickElement("email", self.feed["author_email"])
376 if self.feed["author_link"] is not None:
377 handler.addQuickElement("uri", self.feed["author_link"])
378 handler.endElement("author")
379 if self.feed["subtitle"] is not None:
380 handler.addQuickElement("subtitle", self.feed["subtitle"])
381 for cat in self.feed["categories"]:
382 handler.addQuickElement("category", "", {"term": cat})
383 if self.feed["feed_copyright"] is not None:
384 handler.addQuickElement("rights", self.feed["feed_copyright"])
386 def write_items(self, handler):
387 for item in self.items:
388 handler.startElement("entry", self.item_attributes(item))
389 self.add_item_elements(handler, item)
390 handler.endElement("entry")
392 def add_item_elements(self, handler, item):
393 handler.addQuickElement("title", item["title"])
394 handler.addQuickElement("link", "", {"href": item["link"], "rel": "alternate"})
396 if item["pubdate"] is not None:
397 handler.addQuickElement("published", rfc3339_date(item["pubdate"]))
399 if item["updateddate"] is not None:
400 handler.addQuickElement("updated", rfc3339_date(item["updateddate"]))
402 # Author information.
403 if item["author_name"] is not None:
404 handler.startElement("author", {})
405 handler.addQuickElement("name", item["author_name"])
406 if item["author_email"] is not None:
407 handler.addQuickElement("email", item["author_email"])
408 if item["author_link"] is not None:
409 handler.addQuickElement("uri", item["author_link"])
410 handler.endElement("author")
412 # Unique ID.
413 if item["unique_id"] is not None:
414 unique_id = item["unique_id"]
415 else:
416 unique_id = get_tag_uri(item["link"], item["pubdate"])
417 handler.addQuickElement("id", unique_id)
419 # Summary.
420 if item["description"] is not None:
421 handler.addQuickElement("summary", item["description"], {"type": "html"})
423 # Enclosures.
424 for enclosure in item["enclosures"]:
425 handler.addQuickElement(
426 "link",
427 "",
428 {
429 "rel": "enclosure",
430 "href": enclosure.url,
431 "length": enclosure.length,
432 "type": enclosure.mime_type,
433 },
434 )
436 # Categories.
437 for cat in item["categories"]:
438 handler.addQuickElement("category", "", {"term": cat})
440 # Rights.
441 if item["item_copyright"] is not None:
442 handler.addQuickElement("rights", item["item_copyright"])
445# This isolates the decision of what the system default is, so calling code can
446# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
447DefaultFeed = Rss201rev2Feed