Meilleurs scripts

This commit is contained in:
julien
2026-08-06 16:42:42 +02:00
parent cc1b03b78e
commit 7a0e75442c
6 changed files with 242 additions and 357 deletions
+76 -130
View File
@@ -10,14 +10,14 @@ from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from _schema import SECTION_ALIASES, VOCABULARY_FIELDS
from _schema import LIST_FIELDS, SECTION_ALIASES, SECTION_FIELDS, VOCABULARY_FIELDS
ROOT_DIR = Path(__file__).resolve().parent.parent
CONTENT_DIR = ROOT_DIR / "src" / "content"
TIMEZONE_NAME = "Europe/Paris"
MZL_RE = re.compile(r"(?:mzl-)?([0-9]{1,3})")
FieldValue = str | list[str]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
@@ -48,55 +48,33 @@ def normalize(value: str) -> str:
return unicodedata.normalize("NFC", value.strip())
def toml_string(value: str) -> str:
return json.dumps(value, ensure_ascii=False)
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 validate_identifier(identifier: str, *, allow_leading_hyphen: bool = False) -> str:
"""Valide un identifiant déjà écrit sous sa forme de nom de fichier."""
def validate_identifier(identifier: str, allow_leading_hyphen: bool) -> str:
value = normalize(identifier)
if value.startswith("-") and not allow_leading_hyphen:
raise ValueError("seul le vocabulaire accepte un identifiant commençant par un tiret")
body = value[1:] if value.startswith("-") else value
if not body or value in {".", "..", "_index"}:
if value.startswith("-") and not allow_leading_hyphen:
raise ValueError(
"seul le vocabulaire accepte un identifiant commençant par un tiret"
)
if (
not body
or value in {".", "..", "_index"}
or value != value.lower()
or value.endswith("-")
or "--" in value
or not all(character.isalnum() or character == "-" for character in body)
):
raise ValueError("identifiant de fichier invalide")
if value != value.lower():
raise ValueError("lidentifiant doit être écrit en minuscules")
if value.endswith("-") or "--" in value:
raise ValueError(
"lidentifiant ne doit pas finir par un tiret ni en contenir deux de suite"
)
if not all(character.isalnum() or character == "-" for character in body):
raise ValueError(
"lidentifiant ne doit contenir que des lettres, des chiffres et des tirets"
)
return value
def parse_mzl(identifier: str) -> tuple[str, str]:
value = normalize(identifier)
match = MZL_RE.fullmatch(value)
if match is None:
raise ValueError(
"un signe doit être identifié par un numéro MZL de un à trois chiffres ASCII"
)
number = int(match.group(1))
if not 1 <= number <= 999:
match = MZL_RE.fullmatch(normalize(identifier))
if match is None or not 1 <= int(match.group(1)) <= 999:
raise ValueError("le numéro MZL doit être compris entre 1 et 999")
mzl = f"{number:03d}"
mzl = f"{int(match.group(1)):03d}"
return f"mzl-{mzl}", mzl
@@ -126,88 +104,55 @@ def title_from_identifier(identifier: str) -> str:
return title[:1].upper() + title[1:]
def build_vocabulary(title: str, timestamp: str, nature: str) -> str:
fields = "\n".join(
f"{field} = []" if field == "logograms" else f'{field} = ""'
for field in VOCABULARY_FIELDS[nature]
)
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
[extra]
{fields}
[taxonomies]
natures = [{toml_string(nature)}]
+++
"""
def empty_fields(fields: tuple[str, ...]) -> dict[str, FieldValue]:
return {field: [] if field in LIST_FIELDS else "" for field in fields}
def build_sign(title: str, timestamp: str, mzl: str) -> str:
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
def content_tables(
section: str,
nature: str | None = None,
mzl: str | None = None,
) -> dict[str, dict[str, FieldValue]]:
if section == "vocabulaire":
assert nature is not None
return {
"extra": empty_fields(VOCABULARY_FIELDS[nature]),
"taxonomies": {"natures": [nature]},
}
[extra]
sign = ""
mzl = "{mzl}"
[taxonomies]
lectures = []
+++
"""
tables = {
table: empty_fields(fields)
for table, fields in SECTION_FIELDS[section].items()
}
if section == "signes":
assert mzl is not None
tables["extra"]["mzl"] = mzl
return tables
def build_page(section: str, title: str, timestamp: str) -> str:
if section == "articles":
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
def build_content(
title: str,
timestamp: str,
tables: dict[str, dict[str, FieldValue]],
) -> str:
lines = [
"+++",
f"title = {json.dumps(title, ensure_ascii=False)}",
f"date = {timestamp}",
"# updated = YYYY-MM-DDTHH:MM:SS+HH:MM",
'description = ""',
"draft = true",
]
[extra]
banner = ""
banner_alt = ""
banner_credit = ""
+++
"""
for table, fields in tables.items():
lines.extend(("", f"[{table}]"))
lines.extend(
f"{field} = {json.dumps(value, ensure_ascii=False)}"
for field, value in fields.items()
)
if section == "grammaire":
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
[taxonomies]
themes = []
+++
"""
if section == "textes":
return f"""+++
title = {toml_string(title)}
date = {timestamp}
# updated = YYYY-MM-DDTHH:MM:SS+HH:MM
description = ""
draft = true
[taxonomies]
genres = []
+++
"""
raise ValueError(f"section non prise en charge : {section}")
lines.extend(("+++", ""))
return "\n".join(lines)
def main() -> int:
@@ -218,13 +163,13 @@ def main() -> int:
if section != "vocabulaire" and args.nature is not None:
raise ValueError("--nature est réservé aux fiches de vocabulaire")
timestamp = current_timestamp()
custom_title = normalize(args.title) if args.title is not None else None
nature = None
mzl = None
if section == "signes":
filename, mzl = parse_mzl(args.identifiant)
title = custom_title if custom_title is not None else ""
content = build_sign(title, timestamp, mzl)
else:
filename = validate_identifier(
args.identifiant,
@@ -233,21 +178,27 @@ def main() -> int:
if section == "vocabulaire":
nature = choose_nature(args.nature)
title = custom_title if custom_title is not None else filename
content = build_vocabulary(title, timestamp, nature)
else:
title = (
custom_title
if custom_title is not None
else title_from_identifier(filename)
)
content = build_page(section, title, timestamp)
except (ValueError, RuntimeError) as exc:
timestamp = datetime.now(ZoneInfo("Europe/Paris")).isoformat(
timespec="seconds"
)
content = build_content(
title,
timestamp,
content_tables(section, nature, mzl),
)
except (ValueError, ZoneInfoNotFoundError) as exc:
print(f"Erreur : {exc}", file=sys.stderr)
return 2
destination = CONTENT_DIR / section / f"{filename}.md"
try:
destination.parent.mkdir(parents=True, exist_ok=True)
with destination.open("x", encoding="utf-8") as stream:
stream.write(content)
except FileExistsError:
@@ -257,15 +208,10 @@ def main() -> int:
)
return 1
except OSError as exc:
print(
f"Erreur : impossible de créer {destination.relative_to(ROOT_DIR)} ({exc})",
file=sys.stderr,
)
print(f"Erreur : impossible de créer la fiche ({exc})", file=sys.stderr)
return 1
print(f"Brouillon créé : {destination.relative_to(ROOT_DIR)}")
print("Compléter la fiche, passer draft à false, puis lancer :")
print(" ./scripts/check-content.py")
return 0