dlai-grader 1.22.1__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dlai-grader might be problematic. Click here for more details.
- dlai_grader-2.0.0/MANIFEST.in +3 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/PKG-INFO +7 -11
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader/__init__.py +1 -1
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader/config.py +12 -5
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader/grading.py +58 -30
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader/io.py +68 -18
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader/notebook.py +21 -17
- dlai_grader-2.0.0/dlai_grader/templates/Makefile +57 -0
- dlai_grader-2.0.0/dlai_grader/templates/copy_assignment_sh/extrafile_n +12 -0
- dlai_grader-2.0.0/dlai_grader/templates/copy_assignment_sh/extrafile_y +16 -0
- dlai_grader-2.0.0/dlai_grader/templates/dockerfile/data_n_solution_n +20 -0
- dlai_grader-2.0.0/dlai_grader/templates/dockerfile/data_n_solution_y +21 -0
- dlai_grader-2.0.0/dlai_grader/templates/dockerfile/data_y_solution_n +21 -0
- dlai_grader-2.0.0/dlai_grader/templates/dockerfile/data_y_solution_y +22 -0
- dlai_grader-2.0.0/dlai_grader/templates/entry_py/solution_n_file_n.py +76 -0
- dlai_grader-2.0.0/dlai_grader/templates/entry_py/solution_n_file_y.py +95 -0
- dlai_grader-2.0.0/dlai_grader/templates/entry_py/solution_y_file_n.py +76 -0
- dlai_grader-2.0.0/dlai_grader/templates/entry_py/solution_y_file_y.py +95 -0
- dlai_grader-2.0.0/dlai_grader/templates/grader.py +31 -0
- dlai_grader-2.0.0/dlai_grader/templates.py +301 -0
- dlai_grader-2.0.0/dlai_grader/types.py +8 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader.egg-info/PKG-INFO +7 -11
- dlai_grader-2.0.0/dlai_grader.egg-info/SOURCES.txt +32 -0
- dlai_grader-2.0.0/pyproject.toml +34 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/setup.py +10 -3
- dlai_grader-1.22.1/dlai_grader/py.typed +0 -0
- dlai_grader-1.22.1/dlai_grader/templates.py +0 -262
- dlai_grader-1.22.1/dlai_grader/types.py +0 -7
- dlai_grader-1.22.1/dlai_grader.egg-info/SOURCES.txt +0 -19
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/LICENSE +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/README.md +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader/cli.py +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader/compiler.py +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader.egg-info/dependency_links.txt +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader.egg-info/entry_points.txt +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader.egg-info/requires.txt +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/dlai_grader.egg-info/top_level.txt +0 -0
- {dlai_grader-1.22.1 → dlai_grader-2.0.0}/setup.cfg +0 -0
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dlai-grader
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Grading utilities for DLAI courses
|
|
5
5
|
Home-page: https://github.com/https-deeplearning-ai/grader
|
|
6
6
|
Author: Andres Zarta
|
|
7
|
-
Author-email: andrezb5@gmail.com
|
|
8
|
-
License: MIT
|
|
7
|
+
Author-email: Andres Zarta <andrezb5@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/https-deeplearning-ai/grader
|
|
9
10
|
Classifier: Programming Language :: Python :: 3
|
|
10
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
12
|
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
12
14
|
Description-Content-Type: text/markdown
|
|
13
15
|
License-File: LICENSE
|
|
14
16
|
Requires-Dist: nbformat>=5.1.3
|
|
15
17
|
Requires-Dist: jupytext>=1.13.0
|
|
16
18
|
Dynamic: author
|
|
17
|
-
Dynamic: author-email
|
|
18
|
-
Dynamic: classifier
|
|
19
|
-
Dynamic: description
|
|
20
|
-
Dynamic: description-content-type
|
|
21
19
|
Dynamic: home-page
|
|
22
|
-
Dynamic: license
|
|
23
|
-
Dynamic: requires-dist
|
|
24
|
-
Dynamic: summary
|
|
20
|
+
Dynamic: license-file
|
|
25
21
|
|
|
26
22
|
# grader
|
|
27
23
|
Automatic grading for DLAI courses. Designed to be compatible with Coursera's grading requirements.
|
|
@@ -2,12 +2,11 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
import csv
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Dict
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
def parse_conf(
|
|
9
8
|
path: str = "./.conf",
|
|
10
|
-
) ->
|
|
9
|
+
) -> dict[str, str]:
|
|
11
10
|
"""Parses variables from .conf file
|
|
12
11
|
|
|
13
12
|
Args:
|
|
@@ -45,10 +44,9 @@ class Config:
|
|
|
45
44
|
submission_workdir: str = "./submission/"
|
|
46
45
|
solution_workdir: str = "./solution/"
|
|
47
46
|
solution_file: str = "solution.ipynb"
|
|
48
|
-
solution_file_path: str =
|
|
47
|
+
solution_file_path: str = ""
|
|
49
48
|
submission_file: str = "submission.ipynb"
|
|
50
|
-
submission_file_path: str =
|
|
51
|
-
feedback_file_path: str = "/shared/feedback.json"
|
|
49
|
+
submission_file_path: str = ""
|
|
52
50
|
part_id: str = get_part_id()
|
|
53
51
|
latest_version: str = (
|
|
54
52
|
parse_conf()["GRADER_VERSION"] if os.path.exists("./.conf") else ""
|
|
@@ -56,3 +54,12 @@ class Config:
|
|
|
56
54
|
assignment_name: str = (
|
|
57
55
|
parse_conf()["ASSIGNMENT_NAME"] if os.path.exists("./.conf") else ""
|
|
58
56
|
)
|
|
57
|
+
|
|
58
|
+
def __post_init__(self):
|
|
59
|
+
# This is where we set the file paths after the instance variables are initialized
|
|
60
|
+
self.solution_file_path = os.path.join(
|
|
61
|
+
self.solution_workdir, self.solution_file
|
|
62
|
+
)
|
|
63
|
+
self.submission_file_path = os.path.join(
|
|
64
|
+
self.submission_workdir, self.submission_file
|
|
65
|
+
)
|
|
@@ -1,75 +1,96 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from functools import wraps
|
|
3
|
-
from typing import Any, Callable
|
|
3
|
+
from typing import Any, Callable
|
|
4
4
|
from types import ModuleType
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
@dataclass
|
|
8
8
|
class LearnerSubmission:
|
|
9
|
-
"""Class that represents a file from the learner. Useful when grading does not depend on the notebook"""
|
|
9
|
+
"""Class that represents a file from the learner. Useful when grading does not depend on the notebook."""
|
|
10
10
|
|
|
11
11
|
submission: Any = None
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
learner_submission =
|
|
14
|
+
learner_submission = ModuleType | LearnerSubmission
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@dataclass
|
|
18
18
|
class test_case:
|
|
19
|
-
"""Class that represents a test case"""
|
|
19
|
+
"""Class that represents a test case."""
|
|
20
20
|
|
|
21
21
|
msg: str = ""
|
|
22
22
|
want: Any = None
|
|
23
23
|
got: Any = None
|
|
24
24
|
failed: bool = False
|
|
25
25
|
|
|
26
|
+
def fail(self):
|
|
27
|
+
"""Sets the failed attribute to True."""
|
|
28
|
+
self.failed = True
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
@dataclass
|
|
28
32
|
class aggregated_test_case:
|
|
29
|
-
"""Class that represents an aggregated collection of test cases"""
|
|
33
|
+
"""Class that represents an aggregated collection of test cases."""
|
|
30
34
|
|
|
31
35
|
test_name: str
|
|
32
|
-
tests:
|
|
36
|
+
tests: list[test_case]
|
|
37
|
+
|
|
33
38
|
|
|
39
|
+
def compute_grading_score(test_cases: list[test_case]) -> tuple[float, str]:
|
|
40
|
+
"""
|
|
41
|
+
Computes the score based on the number of failed and total cases.
|
|
34
42
|
|
|
35
|
-
def compute_grading_score(
|
|
36
|
-
test_cases: List[test_case],
|
|
37
|
-
) -> Tuple[float, str]:
|
|
38
|
-
"""Computes the score based on the number of failed and total cases.
|
|
39
43
|
Args:
|
|
40
44
|
test_cases (List): Test cases.
|
|
45
|
+
|
|
41
46
|
Returns:
|
|
42
47
|
Tuple[float, str]: The grade and feedback message.
|
|
43
|
-
"""
|
|
44
48
|
|
|
49
|
+
"""
|
|
45
50
|
num_cases = len(test_cases)
|
|
46
51
|
if num_cases == 0:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"The grader was unable to generate test cases for your implementation. This suggests a bug with your code, please revise your solution and try again.",
|
|
50
|
-
)
|
|
52
|
+
msg = "The grader was unable to generate test cases for your implementation. This suggests a bug with your code, please revise your solution and try again."
|
|
53
|
+
return 0.0, msg
|
|
51
54
|
|
|
52
55
|
failed_cases = [t for t in test_cases if t.failed]
|
|
53
56
|
score = 1.0 - len(failed_cases) / num_cases
|
|
54
|
-
|
|
57
|
+
score = round(score, 2)
|
|
58
|
+
|
|
59
|
+
if not failed_cases:
|
|
60
|
+
msg = "All tests passed! ✅ Congratulations! 🎉"
|
|
61
|
+
return 1.0, msg
|
|
62
|
+
|
|
63
|
+
feedback_msg = ""
|
|
64
|
+
# if print_passed_tests:
|
|
65
|
+
# passed_cases = [t for t in test_cases if not t.failed]
|
|
66
|
+
# for passed_case in passed_cases:
|
|
67
|
+
# feedback_msg += f"✅ {passed_case.msg}.\n"
|
|
55
68
|
|
|
56
69
|
if failed_cases:
|
|
57
|
-
feedback_msg = ""
|
|
58
70
|
for failed_case in failed_cases:
|
|
59
|
-
feedback_msg += f"
|
|
60
|
-
|
|
71
|
+
feedback_msg += f"❌ {failed_case.msg}.\n"
|
|
72
|
+
|
|
73
|
+
if failed_case.want:
|
|
74
|
+
feedback_msg += f"Expected:\n{failed_case.want}\n"
|
|
75
|
+
|
|
76
|
+
if failed_case.got:
|
|
77
|
+
feedback_msg += f"but got:\n{failed_case.got}\n\n"
|
|
61
78
|
|
|
62
79
|
return score, feedback_msg
|
|
63
80
|
|
|
64
81
|
|
|
65
82
|
def compute_aggregated_grading_score(
|
|
66
|
-
aggregated_test_cases:
|
|
67
|
-
) ->
|
|
68
|
-
"""
|
|
83
|
+
aggregated_test_cases: list[aggregated_test_case],
|
|
84
|
+
) -> tuple[float, str]:
|
|
85
|
+
"""
|
|
86
|
+
Computes the score based on the number of failed and total cases.
|
|
87
|
+
|
|
69
88
|
Args:
|
|
70
89
|
aggregated_test_cases (List): Aggregated test cases for every part.
|
|
90
|
+
|
|
71
91
|
Returns:
|
|
72
92
|
Tuple[float, str]: The grade and feedback message.
|
|
93
|
+
|
|
73
94
|
"""
|
|
74
95
|
scores = []
|
|
75
96
|
msgs = []
|
|
@@ -98,11 +119,14 @@ def compute_aggregated_grading_score(
|
|
|
98
119
|
def object_to_grade(
|
|
99
120
|
origin_module: learner_submission,
|
|
100
121
|
*attr_names: str,
|
|
101
|
-
) -> Callable[[Callable[[Any],
|
|
102
|
-
"""
|
|
122
|
+
) -> Callable[[Callable[[Any], list[test_case]]], Callable[[Any], list[test_case]]]:
|
|
123
|
+
"""
|
|
124
|
+
Used as a parameterized decorator to get any number of attributes from a module.
|
|
125
|
+
|
|
103
126
|
Args:
|
|
104
127
|
origin_module (ModuleType): A module.
|
|
105
|
-
attrs_name (Tuple[str, ...]): Names of the attributes to extract from the module.
|
|
128
|
+
*attrs_name (Tuple[str, ...]): Names of the attributes to extract from the module.
|
|
129
|
+
|
|
106
130
|
"""
|
|
107
131
|
|
|
108
132
|
def middle(func):
|
|
@@ -120,11 +144,13 @@ def object_to_grade(
|
|
|
120
144
|
return middle
|
|
121
145
|
|
|
122
146
|
|
|
123
|
-
def print_feedback(test_cases:
|
|
124
|
-
"""
|
|
147
|
+
def print_feedback(test_cases: list[test_case]) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Prints feedback of public unit tests within notebook.
|
|
125
150
|
|
|
126
151
|
Args:
|
|
127
152
|
test_cases (List[test_case]): List of public test cases.
|
|
153
|
+
|
|
128
154
|
"""
|
|
129
155
|
failed_cases = [t for t in test_cases if t.failed]
|
|
130
156
|
feedback_msg = "\033[92m All tests passed!"
|
|
@@ -137,16 +163,18 @@ def print_feedback(test_cases: List[test_case]) -> None:
|
|
|
137
163
|
print(feedback_msg)
|
|
138
164
|
|
|
139
165
|
|
|
140
|
-
def graded_obj_missing(test_cases:
|
|
141
|
-
"""
|
|
166
|
+
def graded_obj_missing(test_cases: list[test_case]) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Check if the object to grade was found in the learned module.
|
|
142
169
|
|
|
143
170
|
Args:
|
|
144
171
|
test_cases (List[test_case]): List of test cases.
|
|
145
172
|
|
|
146
173
|
Returns:
|
|
147
174
|
bool: True if object is missing. False otherwise.
|
|
175
|
+
|
|
148
176
|
"""
|
|
149
|
-
if len(test_cases) == 1 and test_cases[0].got
|
|
177
|
+
if len(test_cases) == 1 and test_cases[0].got is type(None):
|
|
150
178
|
return True
|
|
151
179
|
|
|
152
180
|
return False
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import json
|
|
2
|
+
import os
|
|
3
3
|
import shutil
|
|
4
|
-
import tarfile
|
|
5
|
-
import nbformat
|
|
6
|
-
import jupytext
|
|
7
4
|
import subprocess
|
|
5
|
+
import tarfile
|
|
6
|
+
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
|
8
7
|
from os import devnull
|
|
8
|
+
from pathlib import Path
|
|
9
9
|
from textwrap import dedent
|
|
10
10
|
from zipfile import ZipFile
|
|
11
|
+
|
|
12
|
+
import jupytext
|
|
13
|
+
import nbformat
|
|
11
14
|
from nbformat.notebooknode import NotebookNode
|
|
12
|
-
|
|
15
|
+
|
|
13
16
|
from .notebook import (
|
|
14
17
|
add_metadata_all_code_cells,
|
|
15
18
|
add_metadata_code_cells_without_pattern,
|
|
16
|
-
tag_code_cells,
|
|
17
19
|
solution_to_learner_format,
|
|
20
|
+
tag_code_cells,
|
|
18
21
|
)
|
|
19
22
|
from .templates import load_templates
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
@contextmanager
|
|
23
26
|
def suppress_stdout_stderr():
|
|
24
|
-
"""A context manager that redirects stdout and stderr to devnull"""
|
|
27
|
+
"""A context manager that redirects stdout and stderr to devnull."""
|
|
25
28
|
with open(devnull, "w") as fnull:
|
|
26
29
|
with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out:
|
|
27
30
|
yield (err, out)
|
|
@@ -30,11 +33,15 @@ def suppress_stdout_stderr():
|
|
|
30
33
|
def read_notebook(
|
|
31
34
|
path: str,
|
|
32
35
|
) -> NotebookNode:
|
|
33
|
-
"""
|
|
36
|
+
"""
|
|
37
|
+
Reads a notebook found in the given path and returns a serialized version.
|
|
38
|
+
|
|
34
39
|
Args:
|
|
35
40
|
path (str): Path of the notebook file to read.
|
|
41
|
+
|
|
36
42
|
Returns:
|
|
37
43
|
NotebookNode: Representation of the notebook following nbformat convention.
|
|
44
|
+
|
|
38
45
|
"""
|
|
39
46
|
return nbformat.read(path, as_version=nbformat.NO_CONVERT)
|
|
40
47
|
|
|
@@ -42,10 +49,12 @@ def read_notebook(
|
|
|
42
49
|
def tag_notebook(
|
|
43
50
|
path: str,
|
|
44
51
|
) -> None:
|
|
45
|
-
"""
|
|
52
|
+
"""
|
|
53
|
+
Adds 'graded' tag to all code cells of a notebook.
|
|
46
54
|
|
|
47
55
|
Args:
|
|
48
56
|
path (str): Path to the notebook.
|
|
57
|
+
|
|
49
58
|
"""
|
|
50
59
|
nb = read_notebook(path)
|
|
51
60
|
nb = tag_code_cells(nb)
|
|
@@ -53,10 +62,12 @@ def tag_notebook(
|
|
|
53
62
|
|
|
54
63
|
|
|
55
64
|
def undeletable_notebook(path: str) -> None:
|
|
56
|
-
"""
|
|
65
|
+
"""
|
|
66
|
+
Makes all code cells of a notebook non-deletable.
|
|
57
67
|
|
|
58
68
|
Args:
|
|
59
69
|
path (str): Path to the notebook.
|
|
70
|
+
|
|
60
71
|
"""
|
|
61
72
|
nb = read_notebook(path)
|
|
62
73
|
nb = add_metadata_all_code_cells(nb, {"deletable": False})
|
|
@@ -64,13 +75,17 @@ def undeletable_notebook(path: str) -> None:
|
|
|
64
75
|
|
|
65
76
|
|
|
66
77
|
def uneditable_notebook(path: str) -> None:
|
|
67
|
-
"""
|
|
78
|
+
"""
|
|
79
|
+
Makes all non-graded code cells of a notebook non-editable.
|
|
68
80
|
|
|
69
81
|
Args:
|
|
70
82
|
path (str): Path to the notebook.
|
|
83
|
+
|
|
71
84
|
"""
|
|
72
85
|
nb = read_notebook(path)
|
|
73
|
-
nb = add_metadata_code_cells_without_pattern(
|
|
86
|
+
nb = add_metadata_code_cells_without_pattern(
|
|
87
|
+
nb, {"editable": False}, ignore_pattern="^# EDITABLE"
|
|
88
|
+
)
|
|
74
89
|
jupytext.write(nb, path)
|
|
75
90
|
|
|
76
91
|
|
|
@@ -79,12 +94,14 @@ def extract_tar(
|
|
|
79
94
|
destination: str,
|
|
80
95
|
post_cleanup: bool = True,
|
|
81
96
|
) -> None:
|
|
82
|
-
"""
|
|
97
|
+
"""
|
|
98
|
+
Extracts a tar file unto the desired destination.
|
|
83
99
|
|
|
84
100
|
Args:
|
|
85
101
|
file_path (str): Path to tar file.
|
|
86
102
|
destination (str): Path where to save uncompressed files.
|
|
87
103
|
post_cleanup (bool, optional): If true, deletes the compressed tar file. Defaults to True.
|
|
104
|
+
|
|
88
105
|
"""
|
|
89
106
|
with tarfile.open(file_path, "r") as my_tar:
|
|
90
107
|
my_tar.extractall(destination)
|
|
@@ -118,14 +135,16 @@ def send_feedback(
|
|
|
118
135
|
feedback_path: str = "/shared/feedback.json",
|
|
119
136
|
err: bool = False,
|
|
120
137
|
) -> None:
|
|
121
|
-
"""
|
|
138
|
+
"""
|
|
139
|
+
Sends feedback to the learner.
|
|
140
|
+
|
|
122
141
|
Args:
|
|
123
142
|
score (float): Grading score to show on Coursera for the assignment.
|
|
124
143
|
msg (str): Message providing additional feedback.
|
|
125
144
|
feedback_path (str): Path where the json feedback will be saved. Defaults to /shared/feedback.json
|
|
126
145
|
err (bool, optional): True if there was an error while grading. Defaults to False.
|
|
127
|
-
"""
|
|
128
146
|
|
|
147
|
+
"""
|
|
129
148
|
post = {"fractionalScore": score, "feedback": msg}
|
|
130
149
|
print(json.dumps(post))
|
|
131
150
|
|
|
@@ -166,7 +185,7 @@ def update_grader_version() -> str:
|
|
|
166
185
|
|
|
167
186
|
new_lines = []
|
|
168
187
|
for l in lines:
|
|
169
|
-
if ("GRADER_VERSION" in l) and (
|
|
188
|
+
if ("GRADER_VERSION" in l) and ("TAG_ID" not in l):
|
|
170
189
|
_, v = l.split("=")
|
|
171
190
|
num_v = int(v)
|
|
172
191
|
new_v = num_v + 1
|
|
@@ -227,13 +246,23 @@ def init_grader() -> None:
|
|
|
227
246
|
write_file_from_template("./Makefile", template_dict["makefile"])
|
|
228
247
|
write_file_from_template("./.conf", template_dict["conf"])
|
|
229
248
|
write_file_from_template("./entry.py", template_dict["entry_py"])
|
|
249
|
+
# write_file_from_template(
|
|
250
|
+
# "./copy_assignment_to_submission.sh",
|
|
251
|
+
# template_dict["copy_assignment_to_submission_sh"],
|
|
252
|
+
# )
|
|
230
253
|
write_file_from_template("./requirements.txt", "dlai-grader")
|
|
231
|
-
|
|
254
|
+
write_file_from_template("./.env", "")
|
|
255
|
+
|
|
232
256
|
os.makedirs("learner")
|
|
233
257
|
os.makedirs("mount")
|
|
234
|
-
os.makedirs("solution")
|
|
235
258
|
os.makedirs("submission")
|
|
236
259
|
|
|
260
|
+
if "COPY data/ /grader/data/" in template_dict["dockerfile"]:
|
|
261
|
+
os.makedirs("data")
|
|
262
|
+
|
|
263
|
+
if "COPY solution/ /grader/solution/" in template_dict["dockerfile"]:
|
|
264
|
+
os.makedirs("solution")
|
|
265
|
+
|
|
237
266
|
|
|
238
267
|
def generate_learner_version(
|
|
239
268
|
filename_source: str,
|
|
@@ -285,3 +314,24 @@ def grade_parts(
|
|
|
285
314
|
subprocess.run(cmd, shell=True, executable="/bin/bash")
|
|
286
315
|
except Exception as e:
|
|
287
316
|
print(f"There was an error grading with coursera_autograder. Details: {e}")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def generate_learner_file(
|
|
320
|
+
filename_source: str,
|
|
321
|
+
filename_target: str,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""
|
|
324
|
+
Generates the learning facing version of any file.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
filename_source (str): Path to original notebook.
|
|
328
|
+
filename_target (str): Path where to save reformatted notebook.
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
solution_code = Path(filename_source).read_text()
|
|
332
|
+
|
|
333
|
+
# format the code to replace with placeholders
|
|
334
|
+
fmt_code = solution_to_learner_format(solution_code)
|
|
335
|
+
|
|
336
|
+
# save the learner files
|
|
337
|
+
Path(filename_target).write_text(fmt_code, encoding="utf-8")
|
|
@@ -72,8 +72,8 @@ def keep_tagged_cells(
|
|
|
72
72
|
filtered_cells = []
|
|
73
73
|
|
|
74
74
|
for cell in notebook["cells"]:
|
|
75
|
-
if (
|
|
76
|
-
not
|
|
75
|
+
if ("tags" not in cell["metadata"]) or (
|
|
76
|
+
tag not in cell["metadata"].get("tags")
|
|
77
77
|
):
|
|
78
78
|
continue
|
|
79
79
|
filtered_cells.append(cell)
|
|
@@ -147,7 +147,7 @@ def get_named_cells(
|
|
|
147
147
|
named_cells = {}
|
|
148
148
|
for cell in notebook["cells"]:
|
|
149
149
|
metadata = cell["metadata"]
|
|
150
|
-
if
|
|
150
|
+
if "name" not in metadata:
|
|
151
151
|
continue
|
|
152
152
|
named_cells.update({metadata.get("name"): cell})
|
|
153
153
|
return named_cells
|
|
@@ -169,12 +169,12 @@ def tag_code_cells(
|
|
|
169
169
|
|
|
170
170
|
for cell in notebook["cells"]:
|
|
171
171
|
if cell["cell_type"] == "code":
|
|
172
|
-
if
|
|
172
|
+
if "tags" not in cell["metadata"]:
|
|
173
173
|
cell["metadata"]["tags"] = []
|
|
174
174
|
|
|
175
175
|
tags = cell["metadata"]["tags"]
|
|
176
176
|
|
|
177
|
-
if not
|
|
177
|
+
if tag not in tags:
|
|
178
178
|
tags.append(tag)
|
|
179
179
|
cell["metadata"]["tags"] = tags
|
|
180
180
|
|
|
@@ -238,27 +238,31 @@ def add_metadata_code_cells_with_pattern(
|
|
|
238
238
|
|
|
239
239
|
|
|
240
240
|
def add_metadata_code_cells_without_pattern(
|
|
241
|
-
notebook: NotebookNode,
|
|
241
|
+
notebook: NotebookNode,
|
|
242
|
+
metadata: dict,
|
|
243
|
+
match_pattern: str = "^# GRADED",
|
|
244
|
+
ignore_pattern: str = None,
|
|
242
245
|
) -> NotebookNode:
|
|
243
|
-
"""Adds metadata to code cells of a notebook that don't match a regexp pattern.
|
|
246
|
+
"""Adds metadata to code cells of a notebook that don't match a regexp pattern and aren't ignored by another pattern.
|
|
247
|
+
|
|
244
248
|
Args:
|
|
245
249
|
notebook (NotebookNode): Notebook to filter.
|
|
246
250
|
metadata (dict): The metadata which should be a key-value pair.
|
|
247
|
-
|
|
251
|
+
match_pattern (str, optional): Pattern to check which cells to add metadata to. Defaults to "^# GRADED ".
|
|
252
|
+
ignore_pattern (str, optional): Pattern to check which cells to ignore. If a cell matches this pattern, it will be skipped.
|
|
253
|
+
|
|
248
254
|
Returns:
|
|
249
255
|
NotebookNode: The notebook with the new metadata.
|
|
250
256
|
"""
|
|
251
|
-
filtered_cells = []
|
|
252
|
-
|
|
253
257
|
for cell in notebook["cells"]:
|
|
254
|
-
if cell["cell_type"] == "code"
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
cell["metadata"] = current_metadata
|
|
258
|
-
|
|
259
|
-
filtered_cells.append(cell)
|
|
258
|
+
if cell["cell_type"] == "code":
|
|
259
|
+
if ignore_pattern and re.search(ignore_pattern, cell["source"]):
|
|
260
|
+
continue
|
|
260
261
|
|
|
261
|
-
|
|
262
|
+
if not re.search(match_pattern, cell["source"]):
|
|
263
|
+
current_metadata = cell.get("metadata", {})
|
|
264
|
+
current_metadata.update(metadata)
|
|
265
|
+
cell["metadata"] = current_metadata
|
|
262
266
|
|
|
263
267
|
return notebook
|
|
264
268
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.PHONY: sync learner build debug-unsafe debug-safe grade versioning tag undeletable uneditable init upgrade coursera zip
|
|
2
|
+
|
|
3
|
+
include .conf
|
|
4
|
+
|
|
5
|
+
PARTIDS = 123 456
|
|
6
|
+
COURSERA_PARTIDS = "123 456"
|
|
7
|
+
|
|
8
|
+
OS := $(shell uname)
|
|
9
|
+
|
|
10
|
+
sync:
|
|
11
|
+
cp mount/submission.ipynb ../$(ASSIGNMENT_NAME)_Solution.ipynb
|
|
12
|
+
cp learner/$(ASSIGNMENT_NAME).ipynb ../$(ASSIGNMENT_NAME).ipynb
|
|
13
|
+
cp mount/$(UNIT_TESTS_NAME).py ../$(UNIT_TESTS_NAME).py
|
|
14
|
+
|
|
15
|
+
learner:
|
|
16
|
+
dlai_grader --learner --output_notebook=./learner/$(ASSIGNMENT_NAME).ipynb
|
|
17
|
+
jupyter nbconvert --clear-output --inplace ./learner/$(ASSIGNMENT_NAME).ipynb
|
|
18
|
+
# rsync -a --exclude="submission.ipynb" --exclude="__pycache__" --exclude=".*" ./mount/ ./learner/
|
|
19
|
+
|
|
20
|
+
build:
|
|
21
|
+
docker build -t $(IMAGE_NAME):$(TAG_ID) .
|
|
22
|
+
|
|
23
|
+
debug-unsafe:
|
|
24
|
+
docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --mount type=bind,source=$(PWD),target=/grader/ --env-file $(PWD)/.env --entrypoint ash $(IMAGE_NAME):$(TAG_ID)
|
|
25
|
+
|
|
26
|
+
debug-safe:
|
|
27
|
+
docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --env-file $(PWD)/.env --entrypoint ash $(IMAGE_NAME):$(TAG_ID)
|
|
28
|
+
|
|
29
|
+
grade:
|
|
30
|
+
docker run -it --rm --memory=$(HARD_MEMORY) --cpus="$(CPUS)" --memory-reservation=$(SOFT_MEMORY) --mount type=bind,source=$(PWD)/mount,target=/shared/submission --env-file $(PWD)/.env --entrypoint ash $(IMAGE_NAME):$(TAG_ID) -c 'for partId in $(PARTIDS); do export partId=$$partId; echo "Processing part $$partId"; python entry.py; done'
|
|
31
|
+
|
|
32
|
+
versioning:
|
|
33
|
+
dlai_grader --versioning
|
|
34
|
+
|
|
35
|
+
tag:
|
|
36
|
+
dlai_grader --tag
|
|
37
|
+
|
|
38
|
+
undeletable:
|
|
39
|
+
dlai_grader --undeletable
|
|
40
|
+
|
|
41
|
+
uneditable:
|
|
42
|
+
dlai_grader --uneditable
|
|
43
|
+
|
|
44
|
+
init:
|
|
45
|
+
dlai_grader --versioning
|
|
46
|
+
dlai_grader --tag
|
|
47
|
+
dlai_grader --undeletable
|
|
48
|
+
dlai_grader --uneditable
|
|
49
|
+
|
|
50
|
+
upgrade:
|
|
51
|
+
dlai_grader --upgrade
|
|
52
|
+
|
|
53
|
+
coursera:
|
|
54
|
+
dlai_grader --grade --partids=$(COURSERA_PARTIDS) --docker=$(IMAGE_NAME):$(TAG_ID) --memory=$(MEMORY_LIMIT) --submission=$(SUB_DIR)
|
|
55
|
+
|
|
56
|
+
zip:
|
|
57
|
+
zip -r $(IMAGE_NAME)$(TAG_ID).zip .
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/bin/bash",
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
Assignment={{ASSIGNMENT_NAME}}
|
|
5
|
+
|
|
6
|
+
SubmissionFile=submission.ipynb
|
|
7
|
+
SubmissionPath=/shared/submission
|
|
8
|
+
SharedDiskPath=/learner_workplace/$UserId/$CourseId/$LessonId
|
|
9
|
+
|
|
10
|
+
# copy synced files (exam image typically sync all files in lesson folder)
|
|
11
|
+
echo "Copy learner submission from $SharedDiskPath/$Assignment to $SubmissionPath/$SubmissionFile"
|
|
12
|
+
cp $SharedDiskPath/$Assignment $SubmissionPath/$SubmissionFile
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash",
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
Assignment={{ASSIGNMENT_NAME}}
|
|
5
|
+
Extra_file={{EXTRA_FILE_NAME}}
|
|
6
|
+
|
|
7
|
+
SubmissionFile=submission.ipynb
|
|
8
|
+
SubmissionPath=/shared/submission
|
|
9
|
+
SharedDiskPath=/learner_workplace/$UserId/$CourseId/$LessonId
|
|
10
|
+
|
|
11
|
+
# copy synced files (exam image typically sync all files in lesson folder)
|
|
12
|
+
echo "Copy learner submission from $SharedDiskPath/$Assignment to $SubmissionPath/$SubmissionFile"
|
|
13
|
+
cp $SharedDiskPath/$Assignment $SubmissionPath/$SubmissionFile
|
|
14
|
+
|
|
15
|
+
echo "Copy learner submission from $SharedDiskPath/$Extra_file to $SubmissionPath/$Extra_file"
|
|
16
|
+
cp $SharedDiskPath/$Extra_file $SubmissionPath/$Extra_file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
FROM continuumio/miniconda3@sha256:d601a04ea48fd45e60808c7072243d33703d29434d2067816b7f26b0705d889a
|
|
2
|
+
|
|
3
|
+
RUN apk update && apk add libstdc++
|
|
4
|
+
|
|
5
|
+
COPY requirements.txt .
|
|
6
|
+
|
|
7
|
+
RUN pip install -r requirements.txt && rm requirements.txt
|
|
8
|
+
|
|
9
|
+
RUN mkdir /grader && \
|
|
10
|
+
mkdir /grader/submission
|
|
11
|
+
|
|
12
|
+
COPY .conf /grader/.conf
|
|
13
|
+
COPY entry.py /grader/entry.py
|
|
14
|
+
COPY grader.py /grader/grader.py
|
|
15
|
+
|
|
16
|
+
RUN chmod a+rwx /grader/
|
|
17
|
+
|
|
18
|
+
WORKDIR /grader/
|
|
19
|
+
|
|
20
|
+
ENTRYPOINT ["python", "entry.py"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
FROM continuumio/miniconda3@sha256:d601a04ea48fd45e60808c7072243d33703d29434d2067816b7f26b0705d889a
|
|
2
|
+
|
|
3
|
+
RUN apk update && apk add libstdc++
|
|
4
|
+
|
|
5
|
+
COPY requirements.txt .
|
|
6
|
+
|
|
7
|
+
RUN pip install -r requirements.txt && rm requirements.txt
|
|
8
|
+
|
|
9
|
+
RUN mkdir /grader && \
|
|
10
|
+
mkdir /grader/submission
|
|
11
|
+
|
|
12
|
+
COPY .conf /grader/.conf
|
|
13
|
+
COPY solution/ /grader/solution/
|
|
14
|
+
COPY entry.py /grader/entry.py
|
|
15
|
+
COPY grader.py /grader/grader.py
|
|
16
|
+
|
|
17
|
+
RUN chmod a+rwx /grader/
|
|
18
|
+
|
|
19
|
+
WORKDIR /grader/
|
|
20
|
+
|
|
21
|
+
ENTRYPOINT ["python", "entry.py"]
|