Files
NLP/lab1/scripts/package_submission.py

209 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Собирает итоговый ZIP-архив лабораторной работы.
Запуск из каталога lab1:
python scripts/package_submission.py
Скрипт копирует только нужные файлы во временный каталог, формирует
MANIFEST.txt и CHECKSUMS.sha256, затем упаковывает всё в
dist/nlp_lab1_submission_YYYYMMDD.zip. Рядом кладётся .sha256 самого архива.
"""
from __future__ import annotations
import argparse
import hashlib
import os
import shutil
import zipfile
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DIST = ROOT / "dist"
DEFAULT_NAME = f"nlp_lab1_submission_{date.today():%Y%m%d}"
EXCLUDE_PREFIXES = (
"data/processed/",
"features/",
"results/",
"nltk_data/",
".venv/",
".git/",
"presentation/preview/",
"report/build/",
".pytest_cache/",
"dist/",
"exports/nlp_lab1_results_",
"exports/nlp_lab1_final_results_20261026_120000.tar.gz.sha256",
)
EXCLUDE_NAMES = {
"stardew_stylometry.bak",
"START_HERE.md",
"ARCHIVE_README.md",
}
LATEX_AUX_SUFFIXES = {".aux", ".log", ".nav", ".out", ".snm", ".toc"}
KEY_FILES = [
"lab1/report/REPORT.pdf",
"lab1/report/REPORT.docx",
"lab1/report/REPORT.md",
"lab1/presentation/stardew_stylometry.pdf",
"lab1/presentation/stardew_stylometry.pptx",
"lab1/presentation/latex/stardew_stylometry.pdf",
"lab1/presentation/latex/stardew_stylometry.tex",
"lab1/FINAL_RESULTS.csv",
"lab1/data/dialogues.csv",
"lab1/exports/nlp_lab1_final_results_20261026_120000.tar.gz",
]
def sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def should_copy(rel: Path) -> bool:
posix = rel.as_posix()
if rel.name in EXCLUDE_NAMES:
return False
if "__pycache__" in rel.parts or rel.suffix == ".pyc":
return False
if rel.suffix in LATEX_AUX_SUFFIXES:
return False
if any(posix == prefix.rstrip("/") or posix.startswith(prefix)
for prefix in EXCLUDE_PREFIXES):
return False
return True
def copy_tree(stage: Path) -> int:
target_root = stage / "lab1"
count = 0
for path in sorted(ROOT.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(ROOT)
if not should_copy(rel):
continue
dst = target_root / rel
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, dst)
count += 1
return count
def write_manifest(stage: Path) -> int:
files = sorted(p for p in stage.rglob("*") if p.is_file())
lines = [
"Архив: " + stage.name,
f"Файлов в манифесте: {len(files)} (сам MANIFEST.txt не включён)",
"",
f"{'SHA-256':64} {'Размер':>12} Путь",
"-" * 100,
]
total = 0
for path in files:
size = path.stat().st_size
total += size
rel = path.relative_to(stage).as_posix()
lines.append(f"{sha256(path)} {size:>12} {rel}")
lines.append("-" * 100)
lines.append(f"Итого байт: {total}")
(stage / "MANIFEST.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
return len(files)
def write_checksums(stage: Path) -> None:
lines = []
for rel_name in KEY_FILES:
path = stage / rel_name
if path.exists():
lines.append(f"{sha256(path)} {rel_name}")
else:
print(f"ВНИМАНИЕ: нет ключевого файла {rel_name}")
(stage / "CHECKSUMS.sha256").write_text("\n".join(lines) + "\n", encoding="utf-8")
def zip_stage(stage: Path, archive: Path) -> None:
archive.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_DEFLATED,
compresslevel=6) as zf:
for path in sorted(stage.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(stage).as_posix()
arcname = f"{stage.name}/{rel}"
info = zipfile.ZipInfo.from_file(path, arcname)
info.compress_type = zipfile.ZIP_DEFLATED
with path.open("rb") as src, zf.open(info, "w") as dst:
shutil.copyfileobj(src, dst)
def human(n: int) -> str:
for unit in ("Б", "КБ", "МБ", "ГБ"):
if n < 1024 or unit == "ГБ":
return f"{n:.1f} {unit}" if unit != "Б" else f"{n} {unit}"
n /= 1024
return f"{n} Б"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--name", default=DEFAULT_NAME, help="имя каталога и ZIP")
parser.add_argument("--keep-stage", action="store_true",
help="не удалять промежуточный каталог dist/stage")
args = parser.parse_args()
name = args.name
stage = DIST / "stage" / name
if stage.exists():
shutil.rmtree(stage)
stage.mkdir(parents=True, exist_ok=True)
print(f"1/4. Копирование файлов в {stage} ...")
copied = copy_tree(stage)
shutil.copy2(ROOT / "START_HERE.md", stage / "START_HERE.md")
shutil.copy2(ROOT / "ARCHIVE_README.md", stage / "ARCHIVE_README.md")
print("2/4. Формирование CHECKSUMS.sha256 ...")
write_checksums(stage)
print("3/4. Формирование MANIFEST.txt ...")
total = write_manifest(stage)
archive = DIST / f"{name}.zip"
print(f"4/4. Упаковка {archive} ...")
zip_stage(stage, archive)
digest = sha256(archive)
(DIST / f"{name}.zip.sha256").write_text(
f"{digest} {archive.name}\n", encoding="utf-8"
)
print()
print(f"Файлов в lab1: {copied}")
print(f"Всего файлов в архиве: {total + 1}")
print(f"Размер ZIP: {human(archive.stat().st_size)}")
print(f"SHA-256 ZIP: {digest}")
print(f"Архив: {archive}")
print(f"Контрольная сумма: {archive}.sha256")
if not args.keep_stage:
shutil.rmtree(stage, ignore_errors=True)
try:
(DIST / "stage").rmdir()
except OSError:
pass
return 0
if __name__ == "__main__":
raise SystemExit(main())