#!/usr/bin/env python3 """Build script: scans shared/, generates zip downloads and public/index.html.""" import html import os import shutil import zipfile from dataclasses import dataclass, field from pathlib import Path SHARED_DIR = Path("shared") PUBLIC_DIR = Path("public") ZIPS_DIR = PUBLIC_DIR / "zips" EXCLUDE_NAMES = {".DS_Store", "__pycache__", ".git", "Thumbs.db"} EXCLUDE_SUFFIXES = {".pyc"} @dataclass class SubSection: name: str zip_url: str files: list[str] @dataclass class Section: title: str subsections: list[SubSection] = field(default_factory=list) zip_url: str | None = None files: list[str] | None = None # --- File filtering --- def should_include(path: Path) -> bool: return path.name not in EXCLUDE_NAMES and path.suffix not in EXCLUDE_SUFFIXES # --- Zip and file listing --- def create_zip(source_dir: Path, zip_path: Path) -> None: """Zip source_dir into zip_path; contents extract under a named folder.""" with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for file_path in sorted(source_dir.rglob("*")): if not file_path.is_file(): continue relative = file_path.relative_to(source_dir.parent) if any(part in EXCLUDE_NAMES for part in relative.parts): continue if not should_include(file_path): continue zf.write(file_path, arcname=relative) def list_files(source_dir: Path) -> list[str]: """Return sorted list of filtered relative file paths for display.""" results = [] for file_path in sorted(source_dir.rglob("*")): if not file_path.is_file(): continue relative = file_path.relative_to(source_dir) if any(part in EXCLUDE_NAMES for part in relative.parts): continue if not should_include(file_path): continue results.append(str(relative)) return results # --- Data model construction --- def scan_shared() -> list[Section]: sections = [] assignments_dir = SHARED_DIR / "assignments" if assignments_dir.exists(): zip_name = "assignments.zip" create_zip(assignments_dir, ZIPS_DIR / zip_name) sections.append(Section( title="Assignments", zip_url=f"zips/{zip_name}", files=list_files(assignments_dir), )) notes_dir = SHARED_DIR / "notes-and-examples" if notes_dir.exists(): date_dirs = sorted( d for d in notes_dir.iterdir() if d.is_dir() and should_include(d) ) subsections = [] for date_dir in date_dirs: zip_name = f"notes-and-examples-{date_dir.name}.zip" create_zip(date_dir, ZIPS_DIR / zip_name) subsections.append(SubSection( name=date_dir.name, zip_url=f"zips/{zip_name}", files=list_files(date_dir), )) sections.append(Section(title="Notes and Examples", subsections=subsections)) return sections # --- HTML rendering --- HTML_TEMPLATE = """\
Lesson files and assignments. Click a download button to get a .zip archive.
{body} """ def render_file_list(files: list[str]) -> str: items = "".join(f"