Skip to content

tutorial RSS feeds #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added _ext/__init__.py
Empty file.
101 changes: 101 additions & 0 deletions _ext/rss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
Create an RSS feed of tutorials

Cribbed from: https://github.com/python/peps/blob/main/pep_sphinx_extensions/generate_rss.py
"""

from dataclasses import dataclass, asdict
from datetime import datetime, UTC
from email.utils import format_datetime
from html import escape
from pprint import pformat
from typing import TYPE_CHECKING
from urllib.parse import urljoin

if TYPE_CHECKING:
from sphinx.application import Sphinx


def _format_rfc_2822(dt: datetime) -> str:
datetime = dt.replace(tzinfo=UTC)
return format_datetime(datetime, usegmt=True)


@dataclass
class RSSItem:
title: str
date: datetime
description: str
url: str
author: str = "pyOpenSci"

@classmethod
def from_meta(cls, page_name: str, meta: dict, app: "Sphinx") -> "RSSItem":
"""Create from a page's metadata"""
url = urljoin(app.config.html_baseurl, app.builder.get_target_uri(page_name))
# purposely don't use `get` here because we want to error if these fields are absent
return RSSItem(
title=meta[":og:title"],
description=meta[":og:description"],
date=datetime.fromisoformat(meta["date"]),
author=meta.get(":og:author", "pyOpenSci"),
url=url,
)

def render(self) -> str:
return f"""\
<item>
<title>{escape(self.title, quote=False)}</title>
<link>{escape(self.url, quote=False)}</link>
<description>{escape(self.description, quote=False)}</description>
<author>{escape(self.author, quote=False)}</author>
<guid isPermaLink="true">{self.url}</guid>
<pubDate>{_format_rfc_2822(self.date)}</pubDate>
</item>"""


@dataclass
class RSSFeed:
items: list[RSSItem]
last_build_date: datetime = datetime.now()
title: str = "pyOpenSci Tutorials"
link: str = "https://www.pyopensci.org/python-package-guide/tutorials/intro.html"
self_link: str = "https://www.pyopensci.org/python-package-guide/tutorials.rss"
description: str = "Tutorials for learning python i guess!!!"
language: str = "en"

def render(self) -> str:
items = sorted(self.items, key=lambda i: i.date, reverse=True)
items = "\n".join([item.render() for item in items])
return f"""\
<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>{self.title}</title>
<link>{self.link}</link>
<atom:link href="{self.self_link}" rel="self"/>
<description>{self.description}</description>
<language>{self.language}</language>
<lastBuildDate>{_format_rfc_2822(self.last_build_date)}</lastBuildDate>
{items}
</channel>
</rss>
"""


def generate_tutorials_feed(app: "Sphinx"):
from sphinx.util import logging

logger = logging.getLogger("_ext.rss")
logger.info("Generating RSS feed for tutorials")
metadata = app.builder.env.metadata
tutorials = [t for t in metadata if t.startswith("tutorials/")]
feed_items = [RSSItem.from_meta(t, metadata[t], app) for t in tutorials]
feed = RSSFeed(items=feed_items)
with open(app.outdir / "tutorials.rss", "w") as f:
f.write(feed.render())

logger.info(
f"Generated RSS feed for tutorials, wrote to {app.outdir / 'tutorials.rss'}"
)
logger.debug(f"feed items: \n{pformat([asdict(item) for item in feed_items])}")
22 changes: 19 additions & 3 deletions conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
from datetime import datetime
import subprocess
import os
from typing import TYPE_CHECKING
from _ext import rss

if TYPE_CHECKING:
from sphinx.application import Sphinx

current_year = datetime.now().year
organization_name = "pyOpenSci"
Expand Down Expand Up @@ -199,3 +204,14 @@
bibtex_bibfiles = ["bibliography.bib"]
# myst complains about bibtex footnotes because of render order
suppress_warnings = ["myst.footnote"]


def _post_build(app: "Sphinx", exception: Exception | None) -> None:
rss.generate_tutorials_feed(app)


def setup(app: "Sphinx"):
app.connect("build-finished", _post_build)

# Parallel safety: https://www.sphinx-doc.org/en/master/extdev/index.html#extension-metadata
return {"parallel_read_safe": True, "parallel_write_safe": True}
6 changes: 6 additions & 0 deletions tutorials/add-license-coc.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: Learn how to add a LICENSE and CODE_OF_CONDUCT file to your Python package. This lesson covers choosing a permissive license, placing key files for visibility on GitHub and PyPI, and adopting the Contributor Covenant to support an inclusive community.
:og:title: Add a License and Code of Conduct to your python package
date: 1970-01-02
---

# Add a `LICENSE` & `CODE_OF_CONDUCT` to your Python package

In the [previous lesson](add-readme) you:
Expand Down
6 changes: 6 additions & 0 deletions tutorials/add-readme.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: Learn how to create a clear, effective README file for your Python package. This lesson covers what to include, why each section matters, and how a well-structured README improves usability and discoverability on GitHub and PyPI.
:og:title: Add a README file to your Python package
date: 1970-01-03
---

# Add a README file to your Python package

In the previous lessons you learned:
Expand Down
6 changes: 6 additions & 0 deletions tutorials/command-line-reference.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: Learn how to add a command-line interface (CLI) to your Python package using the argparse library. This lesson walks you through creating a CLI entry point so users can run your package directly from the terminal.
:og:title: Command Line Reference Guide
date: 1970-01-04
---

# Command Line Reference Guide

```{important}
Expand Down
6 changes: 6 additions & 0 deletions tutorials/get-to-know-hatch.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: Get started with Hatch, a modern Python packaging tool. This lesson introduces Hatch’s features and shows how it simplifies environment management, project scaffolding, and building your package.
:og:title: Get to Know Hatch
date: 1970-01-05
---

# Get to Know Hatch

Our Python packaging tutorials use the tool
Expand Down
3 changes: 2 additions & 1 deletion tutorials/installable-code.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
:og:description: Learn how to make your Python code installable so you can use it across projects.
:og:description: Learn how to make your code installable as a Python package using Hatch. This lesson walks you through structuring your code and configuring pyproject.toml so others can easily install and use your package.
:og:title: Make your Python code installable so it can be used across projects
date: 1970-01-01
---

# Make your Python code installable
Expand Down
6 changes: 6 additions & 0 deletions tutorials/intro.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: This page outlines the key steps to create, document, and share a high-quality scientific Python package. Here you will also get an overview of the pyOpenSci packaging guide and what you’ll learn.
:og:title: Python packaging 101
date: 1970-01-05
---

(packaging-101)=
# Python packaging 101

Expand Down
6 changes: 6 additions & 0 deletions tutorials/publish-conda-forge.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: Learn how to publish your Python package on conda-forge to make it easily installable with conda. This lesson covers the submission process, metadata requirements, and maintaining your feedstock.
:og:title: Publish your Python package that is on PyPI to conda-forge
date: 1970-01-06
---

# Publish your Python package that is on PyPI to conda-forge

In the previous lessons, you've learned:
Expand Down
6 changes: 6 additions & 0 deletions tutorials/publish-pypi.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: Learn how to publish your Python package on PyPI so others can install it using pip. This lesson covers building your package, creating a PyPI account, and uploading your distribution files.
:og:title: Publish your Python package to PyPI
date: 1970-01-07
---

# Publish your Python package to PyPI

:::{todo}
Expand Down
6 changes: 6 additions & 0 deletions tutorials/pyproject-toml.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: The pyproject.toml file is the central configuration file for building and packaging Python projects. This lesson explains key sections like name, version, dependencies, and how they support packaging and distribution. You’ll learn how to set up this file to ensure your package is ready for publishing.
:og:title: Make your Python package PyPI ready - pyproject.toml
date: 1970-01-08
---

# Make your Python package PyPI ready - pyproject.toml

In [the installable code lesson](installable-code), you learned how to add the bare minimum information to a `pyproject.toml` file to make it installable. You then learned how to [publish a bare minimum version of your package to PyPI](publish-pypi.md).
Expand Down
6 changes: 6 additions & 0 deletions tutorials/setup-py-to-pyproject-toml.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
:og:description: If you’re creating a pure Python project, pyproject.toml is preferred over setup.py for packaging and configuration. Learn how to migrate from the older setup.py format to the modern pyproject.toml file. This lesson walks you through updating your package metadata and build settings to align with current Python packaging standards.
:og:title: Using Hatch to Migrate setup.py to a pyproject.toml
date: 1970-01-09
---

# Using Hatch to Migrate setup.py to a pyproject.toml

Hatch can be particularly useful to generate your project's `pyproject.toml` if your project already has a `setup.py`.
Expand Down
Loading