#!/usr/bin/env python from __future__ import annotations import argparse import hashlib import json import shutil import tempfile import zipfile from collections.abc import Iterable from datetime import UTC, datetime from pathlib import Path ROOT = Path(__file__).resolve().parent.parent ROOT_NAME = "VacatureRadar_Project" EXCLUDED_DIRS = { ".git", ".venv", ".pytest_cache", ".ruff_cache", "__pycache__", "backups", "htmlcov", "logs", "media", "staticfiles", } EXCLUDED_FILES = {".coverage", ".env", "celerybeat-schedule", "db.sqlite3"} EXCLUDED_SUFFIXES = {".pyc", ".pyo"} def is_included(path: Path) -> bool: relative = path.relative_to(ROOT) if any(part in EXCLUDED_DIRS for part in relative.parts): return False if path.name in EXCLUDED_FILES or path.suffix in EXCLUDED_SUFFIXES: return False if relative.parts and relative.parts[0] == "local" and path.name != ".gitkeep": return False return path.is_file() def iter_files() -> Iterable[Path]: return sorted(path for path in ROOT.rglob("*") if is_included(path)) def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def write_manifest(staging_root: Path) -> None: entries = [] for path in sorted(staging_root.rglob("*")): if not path.is_file() or path.name == "PROJECT_MANIFEST.json": continue entries.append( { "path": path.relative_to(staging_root).as_posix(), "bytes": path.stat().st_size, "sha256": sha256(path), } ) manifest = { "project": "VacatureRadar", "artifact_root": ROOT_NAME, "generated_at": datetime.now(UTC).isoformat(), "file_count_excluding_manifest": len(entries), "total_bytes_excluding_manifest": sum(item["bytes"] for item in entries), "files": entries, } (staging_root / "PROJECT_MANIFEST.json").write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) def package(output: Path) -> tuple[int, str]: output = output.resolve() output.parent.mkdir(parents=True, exist_ok=True) if output.exists(): output.unlink() with tempfile.TemporaryDirectory(prefix="vacatureradar-package-") as temporary: staging_root = Path(temporary) / ROOT_NAME staging_root.mkdir() for source in iter_files(): relative = source.relative_to(ROOT) destination = staging_root / relative destination.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, destination) write_manifest(staging_root) with zipfile.ZipFile( output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9 ) as archive: for path in sorted(staging_root.rglob("*")): if path.is_file(): archive.write(path, path.relative_to(staging_root.parent)) return output.stat().st_size, sha256(output) def main() -> int: parser = argparse.ArgumentParser(description="Maak een schone VacatureRadar-project-ZIP") parser.add_argument( "output", nargs="?", type=Path, default=ROOT.parent / "VacatureRadar_Project.zip", ) args = parser.parse_args() size, checksum = package(args.output) print(f"ZIP: {args.output.resolve()}") print(f"Bytes: {size}") print(f"SHA256: {checksum}") return 0 if __name__ == "__main__": raise SystemExit(main())