diff --git a/.gitignore b/.gitignore index b1dd850..99145e5 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,6 @@ cython_debug/ .pypirc .DS_Store + +# Generated build output +public/ diff --git a/build.py b/build.py new file mode 100644 index 0000000..82e9e85 --- /dev/null +++ b/build.py @@ -0,0 +1,267 @@ +#!/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"