secubox-deb/packages/secubox-metablogizer/api/site_schema.py
CyberMind-FR 345e59be63 feat(metablog-api): site_schema validator + enricher module (ref #101)
- load_schema/validate use jsonschema Draft7Validator
- enrich() fills version (git describe) + last_updated (git log -1 %cI)
- 8 pytest cases covering valid/invalid/extra fields, with-git/no-git,
  and preservation of existing version

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:18:49 +02:00

73 lines
2.1 KiB
Python

"""
SecuBox-Deb :: MetaBlogizer site.json schema validator + enricher
CyberMind — https://cybermind.fr
Author: Gérald Kerma <gandalf@gk2.net>
License: CMSD-1.0
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
from typing import Any
import jsonschema
SCHEMA_PATH = Path(__file__).parent.parent / "schema" / "site.json.schema.json"
def load_schema() -> dict:
"""Read the JSON Schema once."""
with SCHEMA_PATH.open() as f:
return json.load(f)
def validate(doc: dict) -> tuple[bool, list[str]]:
"""Validate doc against the schema.
Returns (ok, errors). Permissive: doc may have extra fields (the schema
has additionalProperties: true).
"""
schema = load_schema()
validator = jsonschema.Draft7Validator(schema)
errors = [
f"{'.'.join(map(str, e.path)) or '<root>'}: {e.message}"
for e in validator.iter_errors(doc)
]
return (not errors, errors)
def _git(*args: str, cwd: Path) -> str | None:
"""Run git with a 5-second timeout; return stripped stdout or None."""
try:
out = subprocess.run(
["git", "-C", str(cwd), *args],
capture_output=True, text=True, timeout=5,
)
if out.returncode == 0:
return out.stdout.strip() or None
except (FileNotFoundError, subprocess.TimeoutExpired):
pass
return None
def enrich(doc: dict, app_dir: Path) -> dict:
"""Fill derived fields (version, last_updated) if missing.
- version -> git describe --tags --exact-match (else --tags --always)
- last_updated -> git log -1 --format=%cI (RFC 3339)
Does nothing if app_dir has no .git/.
"""
out: dict[str, Any] = dict(doc)
if not (app_dir / ".git").exists():
return out
if not out.get("version"):
out["version"] = (
_git("describe", "--tags", "--exact-match", cwd=app_dir)
or _git("describe", "--tags", "--always", cwd=app_dir)
)
if not out.get("last_updated"):
out["last_updated"] = _git("log", "-1", "--format=%cI", cwd=app_dir)
return out