209 lines
6.4 KiB
Python
209 lines
6.4 KiB
Python
#!/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())
|