Files
VacatureRadar/scripts/package_project.py

143 lines
4.4 KiB
Python

#!/usr/bin/env python
from __future__ import annotations
import argparse
import hashlib
import json
import os
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]:
files: list[Path] = []
for directory, dirnames, filenames in os.walk(ROOT):
dirnames[:] = sorted(name for name in dirnames if name not in EXCLUDED_DIRS)
base = Path(directory)
if base == ROOT / "local":
dirnames.clear()
for filename in sorted(filenames):
path = base / filename
if is_included(path):
files.append(path)
return sorted(files)
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, paths: Iterable[Path] | None = None) -> None:
entries = []
candidates = sorted(paths) if paths is not None else sorted(staging_root.rglob("*"))
for path in candidates:
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(
"--manifest-only",
action="store_true",
help="Werk alleen PROJECT_MANIFEST.json in de repository bij.",
)
parser.add_argument(
"output",
nargs="?",
type=Path,
default=ROOT.parent / "VacatureRadar_Project.zip",
)
args = parser.parse_args()
if args.manifest_only:
write_manifest(ROOT, iter_files())
print(f"Manifest: {(ROOT / 'PROJECT_MANIFEST.json').resolve()}")
return 0
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())