MUSA Graderv0.1.0

API reference · grader/discovery.py · 603 lines

grader.discovery

Find student submissions in a folder or a Canvas export ZIP (design.md §16-17).

Notebook filenames are not standardised, so discovery scores the candidates and refuses to guess when two files look equally plausible — an ambiguous submission is flagged for review rather than graded against the wrong notebook.

Summary

NameKindDescription
SubmissionCandidateclassOne student's submission as found on disk.
choose_notebook()functionReturn the most plausible notebook and any review reasons.
find_notebooks()function
find_data_files()function
extract_zip()functionExtract a Canvas export, refusing path traversal entries.
parse_canvas_filename()functionReturn (student_id, display_name, late) for a flat Canvas filename.
discover_submissions()functionDiscover submissions under root, handling both supported layouts.
place_shared_data()functionPut instructor-supplied data where a notebook expects to find it.
prepare_workdir()functionCopy one submission into an isolated working directory (design.md §17).

Constants

NameValueNotes
NOTEBOOK_SUFFIX'.ipynb'
DATA_SUFFIXES{'.csv', '.xlsx', '.xls', '.json', '.geojson', '.shp', '.dbf', '.shx', '.prj', '.txt', '.parquet', '.gpkg', '.zip'}
SKIP_DIRS{'.ipynb_checkpoints', '__MACOSX', '.git', '__pycache__', '.ipynb_checkpoint'}
CANVAS_REre.compile("^(?P<name>[a-z\\-']+?)(?P<late>_late)?_(?P<sub>\\d+)_(?P<attempt>\\d+)_(?P<file>.+)$", re.IGNORECASE)Canvas flattens submissions to e.g. "doejane_12345_67890_assignment1-2.ipynb", sometimes with a LATE marker.
REASON_MULTIPLE_NOTEBOOKS'multiple_notebooks'
REASON_NO_NOTEBOOK'no_notebook'

Classes

class SubmissionCandidate #[source]

dataclass

One student's submission as found on disk.

Fields

NameTypeDefaultNotes
student_idstrrequired
display_namestr''
source_dirPath | NoneNone
notebook_pathPath | NoneNone
all_notebookslist[Path]field(default_factory=list)
data_fileslist[Path]field(default_factory=list)
warningslist[str]field(default_factory=list)
review_reasonslist[str]field(default_factory=list)
lateboolFalse
data_placementdict[str, Any]field(default_factory=dict)
run_subdirstr''

Properties

property ok: bool #[source]

True when a notebook was chosen for this submission.

property needs_review: bool #[source]

No description.

Methods

summary() -> dict[str, Any] #[source]

No description.

Functions

choose_notebook(notebooks: list[Path], hints: Iterable[str] = (), ignore: Iterable[str] = (), root: Path | None = None) -> tuple[Path | None, list[str]] #[source]

Return the most plausible notebook and any review reasons.

Ties are not broken: design.md §17 says not to guess aggressively.

find_notebooks(directory: Path) -> list[Path] #[source]

No description.

find_data_files(directory: Path, limit: int = 200) -> list[Path] #[source]

No description.

extract_zip(zip_path: str | Path, dest: str | Path) -> Path #[source]

Extract a Canvas export, refusing path traversal entries.

parse_canvas_filename(filename: str) -> tuple[str, str, bool] #[source]

Return (student_id, display_name, late) for a flat Canvas filename.

discover_submissions(root: str | Path, notebook_hints: Iterable[str] = (), ignore_patterns: Iterable[str] = ()) -> list[SubmissionCandidate] #[source]

Discover submissions under root, handling both supported layouts.

Nothing is ever written into root: flat Canvas files are regrouped, and archives expanded, in a staging area under the system temp directory.

place_shared_data(workdir: str | Path, shared_files: Iterable[str | Path], referenced_paths: Iterable[str] = (), fallback_dirs: Iterable[str] = ('data', ''), max_placements: int = 12, run_dir: str | Path | None = None, prefer_student_files: bool = False) -> dict[str, Any] #[source]

Put instructor-supplied data where a notebook expects to find it.

Students submit a notebook and nothing else, so the data has to come from the grader. Rather than dictating one filename, this puts the supplied file at every relative path the notebook actually passes to a read_* call, plus the conventional data/<name> and <name> locations. Paths are resolved from the directory the notebook runs in, so ../data/x.csv works too, as long as it stays inside the working directory.

If a student did hand in a data file at the same path, the uploaded file replaces it in the working copy unless prefer_student_files is set, so the whole class is graded on the same data. The student's original is untouched.

prepare_workdir(candidate: SubmissionCandidate, workdir: str | Path, shared_data: Iterable[str | Path] = (), referenced_paths: Iterable[str] = (), fallback_dirs: Iterable[str] = ('data', ''), prefer_student_files: bool = False, max_placements: int = 12) -> Path #[source]

Copy one submission into an isolated working directory (design.md §17).

The submission keeps its own layout, and the notebook runs from the folder it was submitted in, so ../data/x.csv from assignments/hw1.ipynb means what it meant on the student's machine. When the notebook climbs higher than its submission is deep (a lone notebook reading ../data), the tree is nested one level further down so that path still lands inside the working directory. The notebook is renamed notebook.ipynb; the run directory is recorded on candidate.run_subdir and returned.