#!/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(): subdirs = sorted( d for d in assignments_dir.iterdir() if d.is_dir() and should_include(d) ) if subdirs: subsections = [] for subdir in subdirs: zip_name = f"assignments-{subdir.name}.zip" create_zip(subdir, ZIPS_DIR / zip_name) subsections.append(SubSection( name=subdir.name, zip_url=f"zips/{zip_name}", files=list_files(subdir), )) sections.append(Section(title="Assignments", subsections=subsections)) else: 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 = """\ DSA Tutoring

DSA Tutoring

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"
  • {html.escape(f)}
  • \n" for f in files) return f" " def render_download_btn(zip_url: str, label: str) -> str: return ( f"" f"Download {html.escape(label)} (.zip)" ) def render_section(section: Section) -> str: if section.subsections: parts = [] for sub in section.subsections: parts.append( f"
    \n" f"
    \n" f"

    {html.escape(sub.name)}

    \n" f" {render_download_btn(sub.zip_url, sub.name)}\n" f"
    \n" f"{render_file_list(sub.files)}\n" f"
    " ) inner = "\n".join(parts) else: inner = ( f"
    \n" f" {render_download_btn(section.zip_url, section.title)}\n" f"{render_file_list(section.files or [])}\n" f"
    " ) return ( f"
    \n" f"

    {html.escape(section.title)}

    \n" f"{inner}\n" f"
    " ) def render_html(sections: list[Section]) -> str: body = "\n".join(render_section(s) for s in sections) return HTML_TEMPLATE.format(body=body) # --- Entry point --- def main() -> None: os.chdir(Path(__file__).parent) if PUBLIC_DIR.exists(): shutil.rmtree(PUBLIC_DIR) PUBLIC_DIR.mkdir() ZIPS_DIR.mkdir() sections = scan_shared() html_content = render_html(sections) output = PUBLIC_DIR / "index.html" output.write_text(html_content, encoding="utf-8") print(f"Built {output}") print(f"Zips in {ZIPS_DIR}:") for z in sorted(ZIPS_DIR.iterdir()): print(f" {z.name}") if __name__ == "__main__": main()