Browse Source

Feed generation from a stream of notes

master
David Larlet 8 months ago
commit
217ed47074
No known key found for this signature in database
4 changed files with 138 additions and 0 deletions
  1. 1
    0
      .gitignore
  2. 4
    0
      requirements.txt
  3. 95
    0
      runner.py
  4. 38
    0
      utils.py

+ 1
- 0
.gitignore View File

@@ -0,0 +1 @@
__pycache__

+ 4
- 0
requirements.txt View File

@@ -0,0 +1,4 @@
Jinja2==2.10.3
Markdown==3.1.1
MarkupSafe==1.1.1
minicli==0.4.4

+ 95
- 0
runner.py View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3

import os
from dataclasses import dataclass
from datetime import date, datetime
from html import escape
from operator import attrgetter
from pathlib import Path
from time import perf_counter

from jinja2 import Environment as Env
from jinja2 import FileSystemLoader
from minicli import cli, run, wrap
from utils import each_markdown_from, parse_markdown

HERE = Path(".")
DAVID = HERE / "david"
DOMAIN = "https://larlet.fr"
# Hardcoding publication at 12 in Paris timezone.
NORMALIZED_STRFTIME = "%Y-%m-%dT12:00:00+01:00"

environment = Env(loader=FileSystemLoader(str(DAVID / "templates")))


@dataclass
class Note:
title: str
content: str
file_path: str

def __post_init__(self):
suffix = len("/index.md")
prefix = len("YYYY/MM/DD") + suffix
date_str = self.file_path[-prefix:-suffix]
self.url = f"/david/stream/{date_str}/"
self.full_url = f"{DOMAIN}{self.url}"
self.date = datetime.strptime(date_str, "%Y/%m/%d").date()
self.normalized_date = self.date.strftime(NORMALIZED_STRFTIME)
self.escaped_title = escape(self.title)
self.escaped_content = escape(
self.content.replace('href="/', f'href="{DOMAIN}/').replace(
'src="/', f'src="{DOMAIN}/'
)
)
self.extract = self.content.split("</p>", 1)[0] + "</p>"

@staticmethod
def all(source, only_published=True):
"""Retrieve all (published) notes sorted by date desc."""
note_list = []
for file_path in each_markdown_from(source):
title, content, _ = parse_markdown(file_path)
note = Note(title, content, file_path)
if only_published and note.date > date.today():
continue
note_list.append(note)
return sorted(note_list, key=attrgetter("date"), reverse=True)


@cli
def stream(when=None):
"""Create a new note and open it in iA Writer.

:when: Optional date in ISO format (YYYY-MM-DD)
"""
when = datetime.strptime(when, "%Y-%m-%d") if when else date.today()
note_path = DAVID / "stream" / str(when.year) / str(when.month) / str(when.day)
os.makedirs(note_path)
filename = note_path / "index.md"
open(filename, "w+").write("title: ")
os.popen(f'open -a "iA Writer" "{filename}"')


@cli
def feed():
"""Generate a feed from 15 last published Notes in stream."""
template = environment.get_template("feed.xml")
content = template.render(
note_list=Note.all(source=DAVID / "stream" / "2019")[:15],
current_dt=datetime.now().strftime(NORMALIZED_STRFTIME),
BASE_URL=f"{DOMAIN}/david/",
)
open(DAVID / "log" / "index.xml", "w").write(content)


@wrap
def perf_wrapper():
start = perf_counter()
yield
elapsed = perf_counter() - start
print(f"Done in {elapsed:.5f} seconds.")


if __name__ == "__main__":
run()

+ 38
- 0
utils.py View File

@@ -0,0 +1,38 @@
import codecs
import fnmatch
import os

import markdown


def neighborhood(iterable, first=None, last=None):
"""
Yield the (previous, current, next) items given an iterable.

You can specify a `first` and/or `last` item for bounds.
"""
iterator = iter(iterable)
previous = first
current = iterator.next() # Throws StopIteration if empty.
for next in iterator:
yield (previous, current, next)
previous = current
current = next
yield (previous, current, last)


def parse_markdown(file_path):
"""Extract title, (HTML) content and metadata from a markdown file."""
parser = markdown.Markdown(extensions=["meta"])
with codecs.open(file_path, "r") as source:
content = parser.convert(source.read())
metadata = parser.Meta if hasattr(parser, "Meta") else None
title = metadata["title"][0] if metadata is not None else ""
return title, content, metadata


def each_markdown_from(source_dir, file_name="index.md"):
"""Walk across the `source_dir` and return the md file paths."""
for root, dirnames, filenames in os.walk(source_dir):
for filename in fnmatch.filter(filenames, file_name):
yield os.path.join(root, filename)

Loading…
Cancel
Save