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
| Name | Kind | Description |
|---|---|---|
SubmissionCandidate | class | One student's submission as found on disk. |
choose_notebook() | function | Return the most plausible notebook and any review reasons. |
find_notebooks() | function | |
find_data_files() | function | |
extract_zip() | function | Extract a Canvas export, refusing path traversal entries. |
parse_canvas_filename() | function | Return (student_id, display_name, late) for a flat Canvas filename. |
discover_submissions() | function | Discover submissions under root, handling both supported layouts. |
place_shared_data() | function | Put instructor-supplied data where a notebook expects to find it. |
prepare_workdir() | function | Copy one submission into an isolated working directory (design.md §17). |
Constants
| Name | Value | Notes |
|---|---|---|
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_RE | re.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
One student's submission as found on disk.
Fields
| Name | Type | Default | Notes |
|---|---|---|---|
student_id | str | required | |
display_name | str | '' | |
source_dir | Path | None | None | |
notebook_path | Path | None | None | |
all_notebooks | list[Path] | field(default_factory=list) | |
data_files | list[Path] | field(default_factory=list) | |
warnings | list[str] | field(default_factory=list) | |
review_reasons | list[str] | field(default_factory=list) | |
late | bool | False | |
data_placement | dict[str, Any] | field(default_factory=dict) | |
run_subdir | str | '' |
Properties
Methods
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.
- 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.
- 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.csvfromassignments/hw1.ipynbmeans 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 renamednotebook.ipynb; the run directory is recorded oncandidate.run_subdirand returned.