curated_feed.render

Rendering the published window as an Atom feed.

A pure function of state/published.jsonl: the rolling log of everything ever published, denormalised so that rendering cannot be changed by a later edit to the corpus or to a selection.

Four things here are not stylistic choices, because getting any of them wrong makes a reader resurface items that were already read:

  • an entry’s <id> is the canonical article URL, and never changes;

  • an entry’s <updated> is the original publication date, never the run time;

  • the feed’s <updated> moves only when a content hash moves;

  • the same state renders to the same bytes, every time.

The feed itself is public, so its title and author say nothing about the reader.

Attributes

Functions

to_entries(→ list[curated_feed.models.PublishedEntry])

The day's selection as feed entries, in rank order.

unpublished(→ list[curated_feed.models.PublishedEntry])

The entries not already in the log, keyed by the id the feed uses.

window(→ list[curated_feed.models.PublishedEntry])

The newest size entries, newest first.

entry_updated(→ str)

An entry's timestamp: its publication date, never the time of the run.

summary_text(→ str)

The text one entry publishes: the source's description, then the note.

feed_url(→ str)

Where the feed will be served from, or a stable urn if that is unset.

build_feed(→ xml.etree.ElementTree.Element)

The Atom document, as an element tree.

render_atom(→ str)

The feed as text. The same entries and timestamp give the same bytes.

content_hash(→ str)

A hash of everything except the feed timestamp.

render_index(→ str)

A plain landing page, so the URL is not a bare 404 to a curious visitor.

feed_directory(→ pathlib.Path)

Where the feed is written: the build directory plus the secret segment.

write_build(→ pathlib.Path)

Write the feed and its companions, and return the feed's path.

resolve_updated(→ tuple[datetime.datetime, bool])

The feed timestamp, moved only when the content has actually changed.

build_parser(→ argparse.ArgumentParser)

main(→ int)

Module Contents

curated_feed.render.ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom'
curated_feed.render.PUBLISHED_LOG = 'published.jsonl'
curated_feed.render.FEED_META = 'feed.json'
curated_feed.render.FEED_FILENAME = 'feed.xml'
curated_feed.render.INDEX_FILENAME = 'index.html'
curated_feed.render.ROBOTS_FILENAME = 'robots.txt'
curated_feed.render.HEADERS_FILENAME = '_headers'
curated_feed.render.FALLBACK_FEED_ID = 'urn:curated-feed'
curated_feed.render.ROBOTS_TXT = Multiline-String
Show Value
"""User-agent: *
Disallow: /
"""
curated_feed.render.HEADERS = Multiline-String
Show Value
"""/*
  X-Robots-Tag: noindex, nofollow
"""
curated_feed.render.to_entries(selection: curated_feed.models.SelectionResult, candidates: collections.abc.Sequence[curated_feed.models.Candidate]) → list[curated_feed.models.PublishedEntry][source]

The day’s selection as feed entries, in rank order.

curated_feed.render.unpublished(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], published: collections.abc.Sequence[curated_feed.models.PublishedEntry]) → list[curated_feed.models.PublishedEntry][source]

The entries not already in the log, keyed by the id the feed uses.

Re-running a day must not publish it twice, and the entry <id> is the canonical URL, so that is what identity means here.

curated_feed.render.window(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], size: int) → list[curated_feed.models.PublishedEntry][source]

The newest size entries, newest first.

A reader polls on its own schedule and can miss items in a file that churns too fast, so the feed carries far more than one day.

curated_feed.render.entry_updated(entry: curated_feed.models.PublishedEntry) → str[source]

An entry’s timestamp: its publication date, never the time of the run.

curated_feed.render.summary_text(description: str, rationale: str) → str[source]

The text one entry publishes: the source’s description, then the note.

Both go in <summary> rather than one each in <summary> and <content>, because that is the element every reader is certain to show. Splitting them would put the description — the only text worth skimming — behind whether a particular reader renders content at all.

The description comes first because it is what the entry is about; the note is a sentence of provenance and reads as a footer. Either can be empty: an article whose feed gave no description, and a selection whose rationale mode is none, are both ordinary.

>>> summary_text("A panel rejected the argument.", "Reported by 3 outlets.")
'A panel rejected the argument.\n\nReported by 3 outlets.'
>>> summary_text("A panel rejected the argument.", "")
'A panel rejected the argument.'
>>> summary_text("", "")
''
curated_feed.render.feed_url(publish: curated_feed.config.PublishSettings) → str[source]

Where the feed will be served from, or a stable urn if that is unset.

>>> feed_url(PublishSettings(site_url="https://example.com", path_prefix="abc"))
'https://example.com/abc/feed.xml'
curated_feed.render.build_feed(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], *, publish: curated_feed.config.PublishSettings, updated: datetime.datetime) → xml.etree.ElementTree.Element[source]

The Atom document, as an element tree.

Entries are ordered here rather than trusted to arrive ordered, so that the byte-identical guarantee holds for the function and not merely for one caller.

curated_feed.render.render_atom(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], *, publish: curated_feed.config.PublishSettings, updated: datetime.datetime) → str[source]

The feed as text. The same entries and timestamp give the same bytes.

curated_feed.render.content_hash(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], *, publish: curated_feed.config.PublishSettings) → str[source]

A hash of everything except the feed timestamp.

Rendering with a fixed timestamp and hashing the result is what makes the hash cover exactly what a change of <updated> should follow.

curated_feed.render.render_index(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], *, publish: curated_feed.config.PublishSettings) → str[source]

A plain landing page, so the URL is not a bare 404 to a curious visitor.

curated_feed.render.feed_directory(config: curated_feed.config.Config) → pathlib.Path[source]

Where the feed is written: the build directory plus the secret segment.

curated_feed.render.write_build(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], *, config: curated_feed.config.Config, updated: datetime.datetime) → pathlib.Path[source]

Write the feed and its companions, and return the feed’s path.

The feed and its landing page sit under the unguessable segment; the two files that instruct crawlers sit at the root, which is where a host looks for them.

curated_feed.render.resolve_updated(entries: collections.abc.Sequence[curated_feed.models.PublishedEntry], *, config: curated_feed.config.Config, meta_path: pathlib.Path, now: datetime.datetime) → tuple[datetime.datetime, bool][source]

The feed timestamp, moved only when the content has actually changed.

curated_feed.render.build_parser() → argparse.ArgumentParser[source]
curated_feed.render.main(argv: collections.abc.Sequence[str] | None = None) → int[source]