SPB Git

spb/qwhpi Public

QHPI — Quebec Housing Price Index: quality-adjusted, hierarchically pooled housing price indexes.

Python 63.9% TypeScript 25.4% CSS 5.5% TeX 3.5% SQL 0.8% Makefile 0.5% Dockerfile 0.5%
2.8 KB · 92 lines python
Raw Blame History
1#!/usr/bin/env python32# =============================================================================3# QWHPI — Quebec Weekly Housing Price Index4# Author  : Simon-Pierre Boucher5# Contact : contact@spboucher.ai6# File    : scripts/check_headers.py7# Purpose : Pre-commit/CI check that every tracked source file carries the8#           mandatory QWHPI author header (fails with non-zero exit if not).9# =============================================================================10"""Verify that all source files begin with the mandatory QWHPI author header.1112Usage:13    python3 scripts/check_headers.py [paths...]1415With no arguments, scans the whole repository. Exits 1 and lists offending16files if any tracked source file lacks the header.17"""1819from __future__ import annotations2021import sys22from pathlib import Path2324REPO_ROOT = Path(__file__).resolve().parents[1]2526# Extensions that must carry the header.27CHECKED_EXTENSIONS = {28    ".py", ".ts", ".tsx", ".js", ".jsx", ".css", ".sql", ".sh",29    ".yaml", ".yml", ".toml", ".tex",30}31CHECKED_FILENAMES = {"Dockerfile", "Makefile"}3233# Tool-generated files that are overwritten by their generator (a header34# added here would not survive the next build).35GENERATED_FILES = {"next-env.d.ts"}3637# Directories never scanned.38SKIP_DIRS = {39    ".git", "node_modules", ".venv", "venv", "__pycache__", ".next",40    "dist", "build", ".pytest_cache", ".mypy_cache", ".ruff_cache",41    "data", "outputs", ".claude",42}4344REQUIRED_TOKENS = (45    "QWHPI",46    "Simon-Pierre Boucher",47    "contact@spboucher.ai",48)4950# How many leading bytes to inspect for the header block.51HEAD_BYTES = 2048525354def has_header(path: Path) -> bool:55    try:56        head = path.read_text(encoding="utf-8", errors="replace")[:HEAD_BYTES]57    except OSError:58        return False59    return all(token in head for token in REQUIRED_TOKENS)606162def iter_candidates(roots: list[Path]):63    for root in roots:64        if root.is_file():65            yield root66            continue67        for path in sorted(root.rglob("*")):68            if any(part in SKIP_DIRS for part in path.parts):69                continue70            if not path.is_file():71                continue72            if path.name in GENERATED_FILES:73                continue74            if path.suffix in CHECKED_EXTENSIONS or path.name in CHECKED_FILENAMES:75                yield path767778def main(argv: list[str]) -> int:79    roots = [Path(a).resolve() for a in argv[1:]] or [REPO_ROOT]80    missing = [p for p in iter_candidates(roots) if not has_header(p)]81    if missing:82        print("Files missing the mandatory QWHPI author header:")83        for p in missing:84            print(f"  {p.relative_to(REPO_ROOT)}")85        return 186    print("All checked files carry the QWHPI author header.")87    return 0888990if __name__ == "__main__":91    raise SystemExit(main(sys.argv))92