Simplification du script d'horodatage et quelques corrections mineures

This commit is contained in:
julien
2026-08-05 18:29:51 +02:00
parent 65642a5983
commit 81616e11e3
6 changed files with 46 additions and 98 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
"""Définitions communes aux outils de gestion des contenus."""
"""Définitions communes au générateur et au validateur de contenus."""
SECTION_ALIASES = {
"article": "articles",
+38 -90
View File
@@ -4,134 +4,82 @@
import argparse
import re
import sys
import tomllib
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from _schema import KNOWN_SECTIONS
ROOT = Path(__file__).resolve().parent.parent
CONTENT = (ROOT / "src" / "content").resolve()
ROOT_DIR = Path(__file__).resolve().parent.parent
CONTENT_DIR = ROOT_DIR / "src" / "content"
TIMEZONE_NAME = "Europe/Paris"
UPDATED_RE = re.compile(r"\s*(?:#\s*)?updated\s*=.*")
DATE_RE = re.compile(r"\s*date\s*=.*")
FRONT_MATTER_RE = re.compile(
r"\A\+\+\+\r?\n(?P<body>.*?)(?:\r?\n\+\+\+(?=\r?\n|\Z))",
re.DOTALL,
)
UPDATED_RE = re.compile(r"(?m)^(?:#\s*)?updated\s*=.*?(?=\r?$)")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Horodater la dernière modification substantielle dune fiche."
)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"fiche",
help=(
"chemin depuis la racine du dépôt ou depuis src/content/ "
"(par exemple vocabulaire/bītum.md)"
),
help="chemin depuis la racine du dépôt ou depuis src/content/",
)
return parser.parse_args()
def current_timestamp() -> str:
try:
timezone = ZoneInfo(TIMEZONE_NAME)
except ZoneInfoNotFoundError as exc:
raise RuntimeError(
f"fuseau {TIMEZONE_NAME} indisponible ; installer les données de fuseaux horaires"
) from exc
return datetime.now(timezone).isoformat(timespec="seconds")
def resolve_path(value: str) -> Path:
path = Path(value).expanduser()
def resolve_content_path(value: str) -> Path:
supplied = Path(value).expanduser()
if supplied.is_absolute():
path = supplied
elif supplied.parts[:2] == ("src", "content"):
path = ROOT_DIR / supplied
else:
path = CONTENT_DIR / supplied
if not path.is_absolute():
path = ROOT / path if path.parts[:2] == ("src", "content") else CONTENT / path
path = path.resolve()
try:
relative = path.relative_to(CONTENT_DIR.resolve())
except ValueError as exc:
raise ValueError("la fiche doit se trouver dans src/content/") from exc
if not path.is_relative_to(CONTENT):
raise ValueError("la fiche doit se trouver dans src/content/")
if not path.is_file():
raise ValueError(f"fichier introuvable : {path}")
if path.suffix != ".md" or path.name == "_index.md":
raise ValueError("le chemin doit désigner une fiche Markdown, pas un index")
if len(relative.parts) < 2 or relative.parts[0] not in KNOWN_SECTIONS:
raise ValueError("la fiche doit appartenir à une section éditoriale connue")
return path
def update_front_matter(path: Path, timestamp: str) -> None:
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
raise ValueError(f"lecture impossible ({exc})") from exc
def update_timestamp(path: Path) -> str:
text = path.read_bytes().decode("utf-8")
front_matter = FRONT_MATTER_RE.match(text)
lines = text.splitlines(keepends=True)
if not lines or lines[0].rstrip("\r\n") != "+++":
raise ValueError("front matter TOML initial absent")
if front_matter is None:
raise ValueError("front matter TOML délimité par +++ absent")
try:
closing_index = next(
index
for index, line in enumerate(lines[1:], start=1)
if line.rstrip("\r\n") == "+++"
)
except StopIteration as exc:
raise ValueError("délimiteur final +++ absent") from exc
matches = list(UPDATED_RE.finditer(front_matter.group("body")))
if len(matches) != 1:
raise ValueError("la fiche doit contenir exactement une ligne updated")
try:
tomllib.loads("".join(lines[1:closing_index]))
except tomllib.TOMLDecodeError as exc:
raise ValueError(f"front matter TOML invalide ({exc})") from exc
timestamp = datetime.now(ZoneInfo("Europe/Paris")).isoformat(timespec="seconds")
updated_indexes = [
index
for index in range(1, closing_index)
if UPDATED_RE.fullmatch(lines[index].rstrip("\r\n")) is not None
]
if len(updated_indexes) > 1:
raise ValueError("plusieurs champs updated sont présents dans le front matter")
match = matches[0]
offset = front_matter.start("body")
start = offset + match.start()
end = offset + match.end()
newline = "\r\n" if lines[0].endswith("\r\n") else "\n"
updated_line = f"updated = {timestamp}{newline}"
if updated_indexes:
lines[updated_indexes[0]] = updated_line
else:
try:
date_index = next(
index
for index in range(1, closing_index)
if DATE_RE.fullmatch(lines[index].rstrip("\r\n")) is not None
)
except StopIteration as exc:
raise ValueError("champ date absent du front matter") from exc
lines.insert(date_index + 1, updated_line)
try:
path.write_text("".join(lines), encoding="utf-8")
except OSError as exc:
raise ValueError(f"écriture impossible ({exc})") from exc
path.write_bytes(
(text[:start] + f"updated = {timestamp}" + text[end:]).encode("utf-8")
)
return timestamp
def main() -> int:
args = parse_args()
try:
path = resolve_content_path(args.fiche)
timestamp = current_timestamp()
update_front_matter(path, timestamp)
except (ValueError, RuntimeError) as exc:
path = resolve_path(args.fiche)
timestamp = update_timestamp(path)
except (OSError, UnicodeError, ValueError, ZoneInfoNotFoundError) as exc:
print(f"Erreur : {exc}", file=sys.stderr)
return 1
print(f"Fiche mise à jour : {path.relative_to(ROOT_DIR)}")
print(f"Fiche mise à jour : {path.relative_to(ROOT)}")
print(f"updated = {timestamp}")
return 0