Skip to content

Commit 2f928dd

Browse files
sneakers-the-ratlwasser
authored andcommitted
tutorial RSS feeds
1 parent effb635 commit 2f928dd

13 files changed

+175
-3
lines changed

_ext/__init__.py

Whitespace-only changes.

_ext/rss.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Create an RSS feed of tutorials
3+
4+
Cribbed from: https://github.com/python/peps/blob/main/pep_sphinx_extensions/generate_rss.py
5+
"""
6+
7+
from dataclasses import dataclass, asdict
8+
from datetime import datetime, UTC
9+
from email.utils import format_datetime
10+
from html import escape
11+
from pprint import pformat
12+
from typing import TYPE_CHECKING
13+
from urllib.parse import urljoin
14+
15+
from sphinx.util import logging
16+
17+
if TYPE_CHECKING:
18+
from sphinx.application import Sphinx
19+
20+
21+
def _format_rfc_2822(dt: datetime) -> str:
22+
datetime = dt.replace(tzinfo=UTC)
23+
return format_datetime(datetime, usegmt=True)
24+
25+
26+
@dataclass
27+
class RSSItem:
28+
title: str
29+
date: datetime
30+
description: str
31+
url: str
32+
author: str = "PyOpenSci"
33+
34+
@classmethod
35+
def from_meta(cls, page_name: str, meta: dict, app: "Sphinx") -> "RSSMeta":
36+
"""Create from a page's metadata"""
37+
url = urljoin(app.config.html_baseurl, app.builder.get_target_uri(page_name))
38+
# purposely don't use `get` here because we want to error if these fields are absent
39+
return RSSItem(
40+
title=meta[":og:title"],
41+
description=meta[":og:description"],
42+
date=datetime.fromisoformat(meta["date"]),
43+
author=meta.get(":og:author", "PyOpenSci"),
44+
url=url,
45+
)
46+
47+
def render(self) -> str:
48+
return f"""\
49+
<item>
50+
<title>{escape(self.title, quote=False)}</title>
51+
<link>{escape(self.url, quote=False)}</link>
52+
<description>{escape(self.description, quote=False)}</description>
53+
<author>{escape(self.author, quote=False)}</author>
54+
<guid isPermaLink="true">{self.url}</guid>
55+
<pubDate>{_format_rfc_2822(self.date)}</pubDate>
56+
</item>"""
57+
58+
59+
@dataclass
60+
class RSSFeed:
61+
items: list[RSSItem]
62+
last_build_date: datetime = datetime.now()
63+
title: str = "PyOpenSci Tutorials"
64+
link: str = "https://www.pyopensci.org/python-package-guide/tutorials/intro.html"
65+
self_link: str = "https://www.pyopensci.org/python-package-guide/tutorials.rss"
66+
description: str = "Tutorials for learning python i guess!!!"
67+
language: str = "en"
68+
69+
def render(self) -> str:
70+
items = sorted(self.items, key=lambda i: i.date, reverse=True)
71+
items = "\n".join([item.render() for item in items])
72+
return f"""\
73+
<?xml version='1.0' encoding='UTF-8'?>
74+
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
75+
<channel>
76+
<title>{self.title}</title>
77+
<link>{self.link}</link>
78+
<atom:link href="{self.self_link}" rel="self"/>
79+
<description>{self.description}</description>
80+
<language>{self.language}</language>
81+
<lastBuildDate>{_format_rfc_2822(self.last_build_date)}</lastBuildDate>
82+
{items}
83+
</channel>
84+
</rss>
85+
"""
86+
87+
88+
def generate_tutorials_feed(app: "Sphinx"):
89+
logger = logging.getLogger("_ext.rss")
90+
logger.info("Generating RSS feed for tutorials")
91+
metadata = app.builder.env.metadata
92+
tutorials = [t for t in metadata if t.startswith("tutorials/")]
93+
feed_items = [RSSItem.from_meta(t, metadata[t], app) for t in tutorials]
94+
feed = RSSFeed(items=feed_items)
95+
with open(app.outdir / "tutorials.rss", "w") as f:
96+
f.write(feed.render())
97+
98+
logger.info(
99+
f"Generated RSS feed for tutorials, wrote to {app.outdir / 'tutorials.rss'}"
100+
)
101+
logger.debug(f"feed items: \n{pformat([asdict(item) for item in feed_items])}")

conf.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010
# add these directories to sys.path here. If the directory is relative to the
1111
# documentation root, use os.path.abspath to make it absolute, like shown here.
1212
#
13-
# import os
14-
# import sys
15-
# sys.path.insert(0, os.path.abspath('.'))
13+
import os
14+
import sys
15+
sys.path.insert(0, os.path.abspath('.'))
1616
from datetime import datetime
1717
import subprocess
1818
import os
19+
from typing import TYPE_CHECKING
20+
from _ext import rss
21+
22+
if TYPE_CHECKING:
23+
from sphinx.application import Sphinx
1924

2025
current_year = datetime.now().year
2126
organization_name = "pyOpenSci"
@@ -199,3 +204,14 @@
199204
bibtex_bibfiles = ["bibliography.bib"]
200205
# myst complains about bibtex footnotes because of render order
201206
suppress_warnings = ["myst.footnote"]
207+
208+
209+
def _post_build(app: "Sphinx", exception: Exception | None) -> None:
210+
rss.generate_tutorials_feed(app)
211+
212+
213+
def setup(app: "Sphinx"):
214+
app.connect("build-finished", _post_build)
215+
216+
# Parallel safety: https://www.sphinx-doc.org/en/master/extdev/index.html#extension-metadata
217+
return {"parallel_read_safe": True, "parallel_write_safe": True}

tutorials/add-license-coc.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
:og:description: Placeholder text!!!!
3+
:og:title: Add a License and Code of Conduct to your python package
4+
date: 1970-01-02
5+
---
6+
17
# Add a `LICENSE` & `CODE_OF_CONDUCT` to your Python package
28

39
In the [previous lesson](add-readme) you:

tutorials/add-readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
:og:description: Placeholder text!!!!
3+
:og:title: Add a README file to your Python package
4+
date: 1970-01-03
5+
---
6+
17
# Add a README file to your Python package
28

39
In the previous lessons you learned:

tutorials/command-line-reference.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
:og:description: Placeholder text!!!!
3+
:og:title: Command Line Reference Guide
4+
date: 1970-01-04
5+
---
6+
17
# Command Line Reference Guide
28

39
```{important}

tutorials/get-to-know-hatch.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
:og:description: Placeholder text!!!!
3+
:og:title: Get to Know Hatch
4+
date: 1970-01-05
5+
---
6+
17
# Get to Know Hatch
28

39
Our Python packaging tutorials use the tool

tutorials/installable-code.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
:og:description: Learn how to make your Python code installable so you can use it across projects.
33
:og:title: Make your Python code installable so it can be used across projects
4+
date: 1970-01-01
45
---
56

67
# Make your Python code installable

tutorials/intro.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
:og:description: Placeholder text!!!!
3+
:og:title: Python packaging 101
4+
date: 1970-01-05
5+
---
6+
17
(packaging-101)=
28
# Python packaging 101
39

tutorials/publish-conda-forge.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
---
2+
:og:description: Placeholder text!!!!
3+
:og:title: Publish your Python package that is on PyPI to conda-forge
4+
date: 1970-01-06
5+
---
6+
17
# Publish your Python package that is on PyPI to conda-forge
28

39
In the previous lessons, you've learned:

0 commit comments

Comments
 (0)