dlai-grader 1.22.0__py3-none-any.whl → 2.0b1__py3-none-any.whl

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/__init__.py CHANGED
@@ -6,6 +6,6 @@ from . import grading
6
6
  from . import types
7
7
 
8
8
 
9
- __version__ = "1.22.0"
9
+ __version__ = "2.0b1"
10
10
  __author__ = "Andres Zarta"
11
11
  __credits__ = "DeepLearning.AI"
dlai_grader/config.py CHANGED
@@ -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
- ) -> Dict[str, str]:
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 = os.path.join(solution_workdir, solution_file)
47
+ solution_file_path: str = ""
49
48
  submission_file: str = "submission.ipynb"
50
- submission_file_path: str = os.path.join(submission_workdir, submission_file)
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
+ )
dlai_grader/grading.py CHANGED
@@ -1,75 +1,96 @@
1
1
  from dataclasses import dataclass
2
2
  from functools import wraps
3
- from typing import Any, Callable, List, Tuple, Union
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 = Union[ModuleType, LearnerSubmission]
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: List[test_case]
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
- return (
48
- 0.0,
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
- feedback_msg = "All tests passed! Congratulations!"
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"Failed test case: {failed_case.msg}.\nExpected:\n{failed_case.want},\nbut got:\n{failed_case.got}.\n\n"
60
- return round(score, 2), feedback_msg
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: List[aggregated_test_case],
67
- ) -> Tuple[float, str]:
68
- """Computes the score based on the number of failed and total cases.
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], List[test_case]]], Callable[[Any], List[test_case]]]:
102
- """Used as a parameterized decorator to get any number of attributes from a module.
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: List[test_case]) -> None:
124
- """Prints feedback of public unit tests within notebook.
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: List[test_case]) -> bool:
141
- """Check if the object to grade was found in the learned module.
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 == type(None):
177
+ if len(test_cases) == 1 and test_cases[0].got is type(None):
150
178
  return True
151
179
 
152
180
  return False
dlai_grader/io.py CHANGED
@@ -1,27 +1,29 @@
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
9
8
  from textwrap import dedent
10
9
  from zipfile import ZipFile
10
+
11
+ import jupytext
12
+ import nbformat
11
13
  from nbformat.notebooknode import NotebookNode
12
- from contextlib import contextmanager, redirect_stderr, redirect_stdout
14
+
13
15
  from .notebook import (
14
16
  add_metadata_all_code_cells,
15
17
  add_metadata_code_cells_without_pattern,
16
- tag_code_cells,
17
18
  solution_to_learner_format,
19
+ tag_code_cells,
18
20
  )
19
21
  from .templates import load_templates
20
22
 
21
23
 
22
24
  @contextmanager
23
25
  def suppress_stdout_stderr():
24
- """A context manager that redirects stdout and stderr to devnull"""
26
+ """A context manager that redirects stdout and stderr to devnull."""
25
27
  with open(devnull, "w") as fnull:
26
28
  with redirect_stderr(fnull) as err, redirect_stdout(fnull) as out:
27
29
  yield (err, out)
@@ -30,11 +32,15 @@ def suppress_stdout_stderr():
30
32
  def read_notebook(
31
33
  path: str,
32
34
  ) -> NotebookNode:
33
- """Reads a notebook found in the given path and returns a serialized version.
35
+ """
36
+ Reads a notebook found in the given path and returns a serialized version.
37
+
34
38
  Args:
35
39
  path (str): Path of the notebook file to read.
40
+
36
41
  Returns:
37
42
  NotebookNode: Representation of the notebook following nbformat convention.
43
+
38
44
  """
39
45
  return nbformat.read(path, as_version=nbformat.NO_CONVERT)
40
46
 
@@ -42,10 +48,12 @@ def read_notebook(
42
48
  def tag_notebook(
43
49
  path: str,
44
50
  ) -> None:
45
- """Adds 'graded' tag to all code cells of a notebook.
51
+ """
52
+ Adds 'graded' tag to all code cells of a notebook.
46
53
 
47
54
  Args:
48
55
  path (str): Path to the notebook.
56
+
49
57
  """
50
58
  nb = read_notebook(path)
51
59
  nb = tag_code_cells(nb)
@@ -53,10 +61,12 @@ def tag_notebook(
53
61
 
54
62
 
55
63
  def undeletable_notebook(path: str) -> None:
56
- """Makes all code cells of a notebook non-deletable.
64
+ """
65
+ Makes all code cells of a notebook non-deletable.
57
66
 
58
67
  Args:
59
68
  path (str): Path to the notebook.
69
+
60
70
  """
61
71
  nb = read_notebook(path)
62
72
  nb = add_metadata_all_code_cells(nb, {"deletable": False})
@@ -64,13 +74,17 @@ def undeletable_notebook(path: str) -> None:
64
74
 
65
75
 
66
76
  def uneditable_notebook(path: str) -> None:
67
- """Makes all non-graded code cells of a notebook non-editable.
77
+ """
78
+ Makes all non-graded code cells of a notebook non-editable.
68
79
 
69
80
  Args:
70
81
  path (str): Path to the notebook.
82
+
71
83
  """
72
84
  nb = read_notebook(path)
73
- nb = add_metadata_code_cells_without_pattern(nb, {"editable": False})
85
+ nb = add_metadata_code_cells_without_pattern(
86
+ nb, {"editable": False}, ignore_pattern="^# EDITABLE"
87
+ )
74
88
  jupytext.write(nb, path)
75
89
 
76
90
 
@@ -79,12 +93,14 @@ def extract_tar(
79
93
  destination: str,
80
94
  post_cleanup: bool = True,
81
95
  ) -> None:
82
- """Extracts a tar file unto the desired destination.
96
+ """
97
+ Extracts a tar file unto the desired destination.
83
98
 
84
99
  Args:
85
100
  file_path (str): Path to tar file.
86
101
  destination (str): Path where to save uncompressed files.
87
102
  post_cleanup (bool, optional): If true, deletes the compressed tar file. Defaults to True.
103
+
88
104
  """
89
105
  with tarfile.open(file_path, "r") as my_tar:
90
106
  my_tar.extractall(destination)
@@ -118,14 +134,16 @@ def send_feedback(
118
134
  feedback_path: str = "/shared/feedback.json",
119
135
  err: bool = False,
120
136
  ) -> None:
121
- """Sends feedback to the learner.
137
+ """
138
+ Sends feedback to the learner.
139
+
122
140
  Args:
123
141
  score (float): Grading score to show on Coursera for the assignment.
124
142
  msg (str): Message providing additional feedback.
125
143
  feedback_path (str): Path where the json feedback will be saved. Defaults to /shared/feedback.json
126
144
  err (bool, optional): True if there was an error while grading. Defaults to False.
127
- """
128
145
 
146
+ """
129
147
  post = {"fractionalScore": score, "feedback": msg}
130
148
  print(json.dumps(post))
131
149
 
@@ -166,7 +184,7 @@ def update_grader_version() -> str:
166
184
 
167
185
  new_lines = []
168
186
  for l in lines:
169
- if ("GRADER_VERSION" in l) and (not "TAG_ID" in l):
187
+ if ("GRADER_VERSION" in l) and ("TAG_ID" not in l):
170
188
  _, v = l.split("=")
171
189
  num_v = int(v)
172
190
  new_v = num_v + 1
@@ -227,13 +245,23 @@ def init_grader() -> None:
227
245
  write_file_from_template("./Makefile", template_dict["makefile"])
228
246
  write_file_from_template("./.conf", template_dict["conf"])
229
247
  write_file_from_template("./entry.py", template_dict["entry_py"])
248
+ write_file_from_template(
249
+ "./copy_assignment_to_submission.sh",
250
+ template_dict["copy_assignment_to_submission_sh"],
251
+ )
230
252
  write_file_from_template("./requirements.txt", "dlai-grader")
231
- os.makedirs("data")
253
+ write_file_from_template("./.env", "")
254
+
232
255
  os.makedirs("learner")
233
256
  os.makedirs("mount")
234
- os.makedirs("solution")
235
257
  os.makedirs("submission")
236
258
 
259
+ if "COPY data/ /grader/data/" in template_dict["dockerfile"]:
260
+ os.makedirs("data")
261
+
262
+ if "COPY solution/ /grader/solution/" in template_dict["dockerfile"]:
263
+ os.makedirs("solution")
264
+
237
265
 
238
266
  def generate_learner_version(
239
267
  filename_source: str,
dlai_grader/notebook.py CHANGED
@@ -72,8 +72,8 @@ def keep_tagged_cells(
72
72
  filtered_cells = []
73
73
 
74
74
  for cell in notebook["cells"]:
75
- if (not "tags" in cell["metadata"]) or (
76
- not tag in cell["metadata"].get("tags")
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 not "name" in metadata:
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 not "tags" in cell["metadata"]:
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 tag in tags:
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, metadata: dict, regex_pattern: str = "^# GRADED "
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
- regex_pattern (str, optional): Pattern to check. Defaults to "w[1-9]_unittest".
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" and not re.search(regex_pattern, cell["source"]):
255
- current_metadata = cell["metadata"]
256
- current_metadata.update(metadata)
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
- notebook["cells"] = filtered_cells
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
 
dlai_grader/templates.py CHANGED
@@ -1,77 +1,339 @@
1
+ import sys
1
2
  from textwrap import dedent
2
- from typing import Dict
3
3
 
4
4
 
5
- def load_templates() -> Dict[str, str]:
6
- specialization = input("Name of the specialization: ")
7
- course = input("Number of the course: ")
8
- week_or_module = input("Weeks or Modules?\n1 for weeks\n2 for modules: ")
9
-
10
- if week_or_module == "1":
11
- week = input("Number of the week: ")
12
- module = None
13
- elif week_or_module == "2":
14
- module = input("Number of the module: ")
15
- week = None
16
- else:
17
- print("invalid option selected")
18
- exit(1)
5
+ def generate_copy_assignment_script(
6
+ extra_file_required="n",
7
+ assignment_name="C1M2_Assignment.ipynb",
8
+ extra_file_name="foo.txt",
9
+ ):
10
+ """
11
+ Generate copy_assignment_to_submission.sh script with optional extra file copying.
19
12
 
20
- unit_test_filename = input("Filename for unit tests (leave empty for unittests): ")
21
- unit_test_filename = "unittests" if not unit_test_filename else unit_test_filename
22
- version = input("Version of the grader (leave empty for version 1): ")
23
- version = "1" if not version else version
13
+ Args:
14
+ extra_file_required (str): Include extra file copying if "y"
15
+ assignment_name (str): The name of the assignment notebook file
16
+ extra_file_name (str): The name of the extra file to copy (if required)
17
+
18
+ Returns:
19
+ str: The complete script content
20
+
21
+ """
22
+ # Common script header and initial variables
23
+ header = [
24
+ "#!/bin/bash",
25
+ "set -euo pipefail",
26
+ "",
27
+ "# each grader should modify Assignment and Submission file to fulfill the grader setting",
28
+ f"Assignment={assignment_name}",
29
+ ]
30
+
31
+ # Add extra file declaration if required
32
+ if extra_file_required == "y":
33
+ header.append(f"Extra_file={extra_file_name}")
34
+
35
+ # Common script variables
36
+ variables = [
37
+ "",
38
+ "SubmissionFile=submission.ipynb",
39
+ "SubmissionPath=/shared/submission",
40
+ "SharedDiskPath=/learner_workplace/$UserId/$CourseId/$LessonId",
41
+ "",
42
+ "# copy synced files (exam image typically sync all files in lesson folder)",
43
+ 'echo "Copy learner submission from $SharedDiskPath/$Assignment to $SubmissionPath/$SubmissionFile"',
44
+ "cp $SharedDiskPath/$Assignment $SubmissionPath/$SubmissionFile",
45
+ ]
46
+
47
+ # Add extra file copying if required
48
+ extra_file_copy = []
49
+ if extra_file_required == "y":
50
+ extra_file_copy = [
51
+ 'echo "Copy learner submission from $SharedDiskPath/$Extra_file to $SubmissionPath/$Extra_file"',
52
+ "cp $SharedDiskPath/$Extra_file $SubmissionPath/$Extra_file",
53
+ ]
54
+
55
+ # Combine all sections
56
+ content = header + variables + extra_file_copy
57
+
58
+ return "\n".join(content)
59
+
60
+
61
+ def generate_entry_py(
62
+ non_notebook_grading="n",
63
+ extra_file_name="foo.txt",
64
+ ):
65
+ """
66
+ Generate entry.py with optional non-notebook grading capability.
24
67
 
25
- dockerfile = """
26
- FROM continuumio/miniconda3@sha256:d601a04ea48fd45e60808c7072243d33703d29434d2067816b7f26b0705d889a
68
+ Args:
69
+ non_notebook_grading (str): Include non-notebook grading if "y"
70
+ extra_file_name (str): Name of extra file to grade
27
71
 
28
- RUN apk update && apk add libstdc++
72
+ Returns:
73
+ str: The complete entry.py content
29
74
 
30
- COPY requirements.txt .
75
+ """
76
+ # Common imports for both versions
77
+ imports = [
78
+ "from dlai_grader.config import Config, get_part_id",
79
+ "from dlai_grader.compiler import compile_partial_module",
80
+ "from dlai_grader.io import read_notebook, copy_submission_to_workdir, send_feedback",
81
+ "from dlai_grader.notebook import keep_tagged_cells",
82
+ "from dlai_grader.grading import (",
83
+ " compute_grading_score,",
84
+ " graded_obj_missing,",
85
+ " LearnerSubmission,",
86
+ ")",
87
+ "from grader import handle_part_id",
88
+ "",
89
+ ]
90
+
91
+ # Notebook grading function (common to both versions)
92
+ notebook_grading_func = [
93
+ "",
94
+ "def notebook_grading(config, compile_solution=False):",
95
+ " try:",
96
+ " nb = read_notebook(config.submission_file_path)",
97
+ " except Exception as e:",
98
+ ' msg = f"There was a problem reading your notebook. Details:\\n{e!s}"',
99
+ " send_feedback(0.0, msg, err=True)",
100
+ "",
101
+ " transformations = [keep_tagged_cells()]",
102
+ " for t in transformations:",
103
+ " nb = t(nb)",
104
+ "",
105
+ " try:",
106
+ ' learner_mod = compile_partial_module(nb, "learner_mod", verbose=False)',
107
+ " except Exception as e:",
108
+ ' msg = f"There was a problem compiling the code from your notebook, please check that you saved before submitting. Details:\\n{e!s}"',
109
+ " send_feedback(0.0, msg, err=True)",
110
+ "",
111
+ " solution_mod = None",
112
+ " if compile_solution:",
113
+ " solution_nb = read_notebook(config.solution_file_path)",
114
+ " for t in transformations:",
115
+ " solution_nb = t(solution_nb)",
116
+ " solution_mod = compile_partial_module(",
117
+ ' solution_nb, "solution_mod", verbose=False',
118
+ " )",
119
+ "",
120
+ " return learner_mod, solution_mod",
121
+ "",
122
+ ]
123
+
124
+ # Non-notebook grading function (only for version with non_notebook_grading)
125
+ non_notebook_grading_func = [
126
+ "",
127
+ "def non_notebook_grading(config):",
128
+ " try:",
129
+ ' with open(config.submission_file_path, "r") as file:',
130
+ " contents = file.read()",
131
+ " except Exception as e:",
132
+ ' msg = f"There was an error reading your submission. Details:\\n{e!s}"',
133
+ " send_feedback(0.0, msg, err=True)",
134
+ "",
135
+ " return LearnerSubmission(submission=contents)",
136
+ "",
137
+ ]
138
+
139
+ # Main function for version without non-notebook grading
140
+ main_func_simple = [
141
+ "def main() -> None:",
142
+ " copy_submission_to_workdir()",
143
+ "",
144
+ " part_id = get_part_id()",
145
+ "",
146
+ " c = Config()",
147
+ "",
148
+ " learner_mod, _ = notebook_grading(c)",
149
+ "",
150
+ " g_func = handle_part_id(part_id)(learner_mod)",
151
+ "",
152
+ " try:",
153
+ " cases = g_func()",
154
+ " except Exception as e:",
155
+ ' msg = f"There was an error grading your submission. Details:\\n{e!s}"',
156
+ " send_feedback(0.0, msg, err=True)",
157
+ "",
158
+ " if graded_obj_missing(cases):",
159
+ ' msg = "Object required for grading not found. If you haven\'t completed the exercise this might be expected. Otherwise, check your solution as grader omits cells that throw errors."',
160
+ " send_feedback(0.0, msg, err=True)",
161
+ "",
162
+ " score, feedback = compute_grading_score(cases)",
163
+ " send_feedback(score, feedback)",
164
+ "",
165
+ ]
166
+
167
+ # Main function for version with non-notebook grading
168
+ main_func_with_non_notebook = [
169
+ "def main() -> None:",
170
+ " copy_submission_to_workdir()",
171
+ "",
172
+ " part_id = get_part_id()",
173
+ "",
174
+ " match part_id:",
175
+ ' case "123":',
176
+ f' c = Config(submission_file="{extra_file_name}")',
177
+ " learner_mod = non_notebook_grading(c)",
178
+ " case _:",
179
+ " c = Config()",
180
+ " learner_mod, _ = notebook_grading(c)",
181
+ "",
182
+ " g_func = handle_part_id(part_id)(learner_mod)",
183
+ "",
184
+ " try:",
185
+ " cases = g_func()",
186
+ " except Exception as e:",
187
+ ' msg = f"There was an error grading your submission. Details:\\n{e!s}"',
188
+ " send_feedback(0.0, msg, err=True)",
189
+ "",
190
+ " if graded_obj_missing(cases):",
191
+ ' msg = "Object required for grading not found. If you haven\'t completed the exercise this might be expected. Otherwise, check your solution as grader omits cells that throw errors."',
192
+ " send_feedback(0.0, msg, err=True)",
193
+ "",
194
+ " score, feedback = compute_grading_score(cases)",
195
+ " send_feedback(score, feedback)",
196
+ "",
197
+ ]
198
+
199
+ # Common script entry point
200
+ entry_point = [
201
+ 'if __name__ == "__main__":',
202
+ " main()",
203
+ "",
204
+ ]
205
+
206
+ # Combine all sections based on configuration
207
+ content = imports + notebook_grading_func
208
+
209
+ if non_notebook_grading == "y":
210
+ content.extend(non_notebook_grading_func)
211
+ content.extend(main_func_with_non_notebook)
212
+ else:
213
+ content.extend(main_func_simple)
31
214
 
32
- RUN pip install -r requirements.txt && \
33
- rm requirements.txt
215
+ content.extend(entry_point)
34
216
 
35
- RUN mkdir /grader && \
36
- mkdir /grader/submission
217
+ return "\n".join(content)
37
218
 
38
- COPY .conf /grader/.conf
39
- COPY data/ /grader/data/
40
- COPY solution/ /grader/solution/
41
- COPY entry.py /grader/entry.py
42
- COPY grader.py /grader/grader.py
43
219
 
44
- RUN chmod a+rwx /grader/
220
+ def generate_dockerfile(data_dir_required="n", sol_dir_required="n"):
221
+ """
222
+ Generate a Dockerfile with optional data and solution directories.
45
223
 
46
- WORKDIR /grader/
224
+ Args:
225
+ data_dir_required (str): Include data directory if "y"
226
+ sol_dir_required (str): Include solution directory if "y"
227
+
228
+ Returns:
229
+ str: The complete Dockerfile content
47
230
 
48
- ENTRYPOINT ["python", "entry.py"]
49
231
  """
232
+ base_content = [
233
+ "FROM continuumio/miniconda3@sha256:d601a04ea48fd45e60808c7072243d33703d29434d2067816b7f26b0705d889a",
234
+ "",
235
+ "RUN apk update && apk add libstdc++",
236
+ "",
237
+ "COPY requirements.txt .",
238
+ "",
239
+ "RUN pip install -r requirements.txt && rm requirements.txt",
240
+ "",
241
+ "RUN mkdir /grader && \\ \nmkdir /grader/submission",
242
+ "",
243
+ "COPY .conf /grader/.conf",
244
+ ]
245
+
246
+ # Add optional file copies based on config
247
+ if data_dir_required == "y":
248
+ base_content.append("COPY data/ /grader/data/")
249
+
250
+ if sol_dir_required == "y":
251
+ base_content.append("COPY solution/ /grader/solution/")
252
+
253
+ # Add final common parts
254
+ base_content.extend(
255
+ [
256
+ "COPY entry.py /grader/entry.py",
257
+ "COPY grader.py /grader/grader.py",
258
+ "",
259
+ "RUN chmod a+rwx /grader/",
260
+ "",
261
+ "WORKDIR /grader/",
262
+ "",
263
+ 'ENTRYPOINT ["python", "entry.py"]',
264
+ ]
265
+ )
50
266
 
51
- if week:
52
- W_OR_M = "W"
53
- W_OR_M_num = week
267
+ return "\n".join(base_content)
54
268
 
55
- if module:
56
- W_OR_M = "M"
57
- W_OR_M_num = module
269
+
270
+ def load_templates() -> dict[str, str]:
271
+ specialization = input("Name of the specialization: ")
272
+ course = input("Number of the course: ")
273
+ module = input("Number of the module: ")
274
+
275
+ unit_test_filename = input("Filename for unit tests (leave empty for unittests): ")
276
+ unit_test_filename = unit_test_filename if unit_test_filename else "unittests"
277
+ version = input("Version of the grader (leave empty for version 1): ")
278
+ version = version if version else "1"
279
+ data_dir_required = input("Do you require a data dir? y/n (leave empty for n): ")
280
+ data_dir_required = data_dir_required if data_dir_required else "n"
281
+
282
+ if data_dir_required not in ["y", "n"]:
283
+ print("invalid option selected")
284
+ sys.exit(1)
285
+
286
+ sol_dir_required = input(
287
+ "Do you require a solution file? y/n (leave empty for n): "
288
+ )
289
+ sol_dir_required = sol_dir_required if sol_dir_required else "n"
290
+ if sol_dir_required not in ["y", "n"]:
291
+ print("invalid option selected")
292
+ sys.exit(1)
293
+
294
+ non_notebook_grading = input(
295
+ "Will you grade a file different from a notebook? y/n (leave empty for n): ",
296
+ )
297
+ non_notebook_grading = non_notebook_grading if non_notebook_grading else "n"
298
+ if non_notebook_grading not in ["y", "n"]:
299
+ print("invalid option selected")
300
+ sys.exit(1)
301
+
302
+ extra_file_name = ""
303
+ if non_notebook_grading == "y":
304
+ extra_file_name = input(
305
+ "Name of the extra file to grade: ",
306
+ )
307
+
308
+ dockerfile = generate_dockerfile(
309
+ data_dir_required=data_dir_required,
310
+ sol_dir_required=sol_dir_required,
311
+ )
58
312
 
59
313
  conf = f"""
60
- ASSIGNMENT_NAME=C{course}{W_OR_M}{W_OR_M_num}_Assignment
314
+ ASSIGNMENT_NAME=C{course}M{module}_Assignment
61
315
  UNIT_TESTS_NAME={unit_test_filename}
62
- IMAGE_NAME={specialization}c{course}{W_OR_M.lower()}{W_OR_M_num}-grader
316
+ IMAGE_NAME={specialization}c{course}m{module}-grader
63
317
  GRADER_VERSION={version}
64
318
  TAG_ID=V$(GRADER_VERSION)
65
319
  SUB_DIR=mount
66
320
  MEMORY_LIMIT=4096
67
321
  """
68
322
 
323
+ assignment_name = f"C{course}M{module}_Assignment.ipynb"
324
+
325
+ copy_assignment_to_submission_sh = generate_copy_assignment_script(
326
+ extra_file_required=non_notebook_grading,
327
+ assignment_name=assignment_name,
328
+ extra_file_name=extra_file_name,
329
+ )
330
+
69
331
  makefile = """
70
- .PHONY: learner build entry submit-solution upgrade test grade mem zip clean upload move-zip move-learner tag undeletable uneditable versioning upgrade sync
332
+ .PHONY: sync learner build debug-unsafe debug-safe grade versioning tag undeletable uneditable init upgrade coursera zip
71
333
 
72
334
  include .conf
73
335
 
74
- PARTIDS = ""
336
+ PARTIDS = 123 456
75
337
  OS := $(shell uname)
76
338
 
77
339
  sync:
@@ -81,15 +343,19 @@ def load_templates() -> Dict[str, str]:
81
343
 
82
344
  learner:
83
345
  dlai_grader --learner --output_notebook=./learner/$(ASSIGNMENT_NAME).ipynb
346
+ rsync -a --exclude="submission.ipynb" --exclude="__pycache__" --exclude=".mypy_cache" ./mount/ ./learner/
84
347
 
85
348
  build:
86
349
  docker build -t $(IMAGE_NAME):$(TAG_ID) .
87
350
 
88
- debug:
89
- docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --mount type=bind,source=$(PWD),target=/grader/ --entrypoint ash $(IMAGE_NAME):$(TAG_ID)
351
+ debug-unsafe:
352
+ 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)
353
+
354
+ debug-safe:
355
+ docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --env-file $(PWD)/.env --entrypoint ash $(IMAGE_NAME):$(TAG_ID)
90
356
 
91
- submit-solution:
92
- cp solution/solution.ipynb mount/submission.ipynb
357
+ grade:
358
+ docker run -it --rm --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'
93
359
 
94
360
  versioning:
95
361
  dlai_grader --versioning
@@ -103,160 +369,70 @@ def load_templates() -> Dict[str, str]:
103
369
  uneditable:
104
370
  dlai_grader --uneditable
105
371
 
372
+ init:
373
+ dlai_grader --versioning
374
+ dlai_grader --tag
375
+ dlai_grader --undeletable
376
+ dlai_grader --uneditable
377
+
106
378
  upgrade:
107
379
  dlai_grader --upgrade
108
380
 
109
- test:
110
- docker run -it --rm --mount type=bind,source=$(PWD)/mount,target=/shared/submission --mount type=bind,source=$(PWD),target=/grader/ --entrypoint pytest $(IMAGE_NAME):$(TAG_ID)
111
-
112
- grade:
113
- dlai_grader --grade --partids=$(PARTIDS) --docker=$(IMAGE_NAME):$(TAG_ID) --memory=$(MEMORY_LIMIT) --submission=$(SUB_DIR)
114
-
115
- mem:
116
- memthis $(PARTIDS)
381
+ coursera:
382
+ dlai_grader --grade --partids="$(PARTIDS)" --docker=$(IMAGE_NAME):$(TAG_ID) --memory=$(MEMORY_LIMIT) --submission=$(SUB_DIR)
117
383
 
118
384
  zip:
119
385
  zip -r $(IMAGE_NAME)$(TAG_ID).zip .
120
386
 
121
- clean:
122
- find . -maxdepth 1 -type f -name "*.zip" -exec rm {} +
123
- docker rm $$(docker ps -qa --no-trunc --filter "status=exited")
124
- docker rmi $$(docker images --filter "dangling=true" -q --no-trunc)
125
-
126
- upload:
127
- coursera_autograder --timeout 1800 upload --grader-memory-limit $(MEMORY_LIMIT) --grading-timeout 1800 $(IMAGE_NAME)$(TAG_ID).zip $(COURSE_ID) $(ITEM_ID) $(PART_ID)
128
-
129
387
  """
130
388
 
131
389
  grader_py = """
132
390
  from types import ModuleType, FunctionType
133
- from typing import Dict, List, Optional
134
391
  from dlai_grader.grading import test_case, object_to_grade
135
392
  from dlai_grader.types import grading_function, grading_wrapper, learner_submission
136
393
 
137
394
 
138
395
  def part_1(
139
- learner_mod: learner_submission, solution_mod: Optional[ModuleType]
396
+ learner_mod: learner_submission,
397
+ solution_mod: ModuleType | None = None,
140
398
  ) -> grading_function:
141
399
  @object_to_grade(learner_mod, "learner_func")
142
- def g(learner_func: FunctionType) -> List[test_case]:
400
+ def g(learner_func: FunctionType) -> list[test_case]:
401
+
402
+ cases: list[test_case] = []
143
403
 
144
404
  t = test_case()
145
405
  if not isinstance(learner_func, FunctionType):
146
- t.failed = True
406
+ t.fail()
147
407
  t.msg = "learner_func has incorrect type"
148
408
  t.want = FunctionType
149
409
  t.got = type(learner_func)
150
410
  return [t]
151
411
 
152
- cases: List[test_case] = []
153
-
154
412
  return cases
155
413
 
156
414
  return g
157
415
 
158
416
 
159
417
  def handle_part_id(part_id: str) -> grading_wrapper:
160
- grader_dict: Dict[str, grading_wrapper] = {
418
+ grader_dict: dict[str, grading_wrapper] = {
161
419
  "": part_1,
162
420
  }
163
421
  return grader_dict[part_id]
164
422
  """
165
423
 
166
- entry_py = """
167
- from dlai_grader.config import Config
168
- from dlai_grader.compiler import compile_partial_module
169
- from dlai_grader.io import read_notebook, copy_submission_to_workdir, send_feedback
170
-
171
- from dlai_grader.notebook import (
172
- notebook_to_script,
173
- keep_tagged_cells,
174
- notebook_is_up_to_date,
175
- notebook_version,
176
- cut_notebook,
177
- partial_grading_enabled,
424
+ entry_py = generate_entry_py(
425
+ non_notebook_grading=non_notebook_grading,
426
+ extra_file_name=extra_file_name,
178
427
  )
179
- from dlai_grader.grading import compute_grading_score, graded_obj_missing
180
- from grader import handle_part_id
181
-
182
-
183
- def main() -> None:
184
- c = Config()
185
-
186
- copy_submission_to_workdir()
187
-
188
- try:
189
- nb = read_notebook(c.submission_file_path)
190
- except Exception as e:
191
- send_feedback(
192
- 0.0,
193
- f"There was a problem reading your notebook. Details:\\n{str(e)}",
194
- err=True,
195
- )
196
-
197
- if not notebook_is_up_to_date(nb):
198
- msg = f"You are submitting a version of the assignment that is behind the latest version.\\nThe latest version is {c.latest_version} and you are on version {notebook_version(nb)}."
199
-
200
- send_feedback(0.0, msg)
201
-
202
- transformations = [cut_notebook(), keep_tagged_cells()]
203
-
204
- for t in transformations:
205
- nb = t(nb)
206
-
207
- try:
208
- learner_mod = compile_partial_module(nb, "learner_mod", verbose=False)
209
- except Exception as e:
210
- send_feedback(
211
- 0.0,
212
- f"There was a problem compiling the code from your notebook, please check that you saved before submitting. Details:\\n{str(e)}",
213
- err=True,
214
- )
215
-
216
- solution_nb = read_notebook(c.solution_file_path)
217
-
218
- for t in transformations:
219
- solution_nb = t(solution_nb)
220
-
221
- solution_mod = compile_partial_module(solution_nb, "solution_mod", verbose=False)
222
-
223
- g_func = handle_part_id(c.part_id)(learner_mod, solution_mod)
224
-
225
- try:
226
- cases = g_func()
227
- except Exception as e:
228
- send_feedback(
229
- 0.0,
230
- f"There was an error grading your submission. Details:\\n{str(e)}",
231
- err=True,
232
- )
233
-
234
- if graded_obj_missing(cases):
235
- additional_msg = ""
236
- if partial_grading_enabled(nb):
237
- additional_msg = "The # grade-up-to-here comment in the notebook might be causing the problem."
238
-
239
- send_feedback(
240
- 0.0,
241
- f"Object required for grading not found. If you haven't completed the exercise this is expected. Otherwise, check your solution as grader omits cells that throw errors.\\n{additional_msg}",
242
- err=True,
243
- )
244
-
245
- score, feedback = compute_grading_score(cases)
246
-
247
- send_feedback(score, feedback)
248
-
249
-
250
- if __name__ == "__main__":
251
- main()
252
- """
253
428
 
254
429
  template_dict = {
255
- "dockerfile": dedent(dockerfile[1:]),
430
+ "dockerfile": dedent(dockerfile),
256
431
  "makefile": dedent(makefile[1:]),
257
432
  "conf": dedent(conf[1:]),
258
433
  "grader_py": dedent(grader_py[1:]),
259
- "entry_py": dedent(entry_py[1:]),
434
+ "entry_py": dedent(entry_py),
435
+ "copy_assignment_to_submission_sh": dedent(copy_assignment_to_submission_sh),
260
436
  }
261
437
 
262
438
  return template_dict
dlai_grader/types.py CHANGED
@@ -1,7 +1,8 @@
1
+ from collections.abc import Callable
1
2
  from types import ModuleType
2
- from typing import Any, Callable, List, Optional, Union
3
- from .grading import test_case, LearnerSubmission
4
3
 
5
- grading_function = Callable[[Any], List[test_case]]
6
- learner_submission = Union[ModuleType, LearnerSubmission]
7
- grading_wrapper = Callable[[learner_submission, Optional[ModuleType]], grading_function]
4
+ from .grading import LearnerSubmission, test_case
5
+
6
+ grading_function = Callable[..., list[test_case]]
7
+ learner_submission = ModuleType | LearnerSubmission
8
+ grading_wrapper = Callable[[learner_submission, ModuleType | None], grading_function]
@@ -1,27 +1,23 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: dlai-grader
3
- Version: 1.22.0
3
+ Version: 2.0b1
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 License
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.
@@ -0,0 +1,16 @@
1
+ dlai_grader/__init__.py,sha256=V0DW6eFVqfyWdxFvJavFxD2QV63rZCpT400IzY1QBkk,210
2
+ dlai_grader/cli.py,sha256=NIwboE-AFn1LXOFmF4O70Ow0fkRxgclG_eMwmWiua38,4917
3
+ dlai_grader/compiler.py,sha256=elbHNUCqBCoOOoNmMRXbgeNL0nt0RM57eZi0-6AqycA,3036
4
+ dlai_grader/config.py,sha256=DokK1tVF_r7v0p9tWpBN-7lOAlPmHSpFXDZiI8cGw7s,1821
5
+ dlai_grader/grading.py,sha256=BMIoZ_loQDuNCEk1Dj3on4IWz-FGgIbnhbDyk8HmQ7c,5041
6
+ dlai_grader/io.py,sha256=TEp5R8014EL9e-_RID5qp9INOujAGi7KLuy-SWWsPkM,8654
7
+ dlai_grader/notebook.py,sha256=MgxZFuetTXwwZ-HXSB5ItLVD_9LP45E0xHAngS0g4EU,12101
8
+ dlai_grader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ dlai_grader/templates.py,sha256=SEMv4KFlq_2jy4UORMzHStOOex9P6LXr48Ie-r1JZcc,14627
10
+ dlai_grader/types.py,sha256=5uiFaF3aDn-vjxTp9ec-ND-PRqeeV2_NfPHS2ngGsRo,306
11
+ dlai_grader-2.0b1.dist-info/licenses/LICENSE,sha256=a_kch_UqdJPtyxk35QJr9O84K_koPixqWPYW9On4-io,1072
12
+ dlai_grader-2.0b1.dist-info/METADATA,sha256=0-7wdm6Otf4nHWASbjMHMBt8ruXvl3RrDakgAVPCMlA,8776
13
+ dlai_grader-2.0b1.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
14
+ dlai_grader-2.0b1.dist-info/entry_points.txt,sha256=4OcSAUIluONXa3ymViQ7CBQ2Lk52nb6xZnfph1rlMnk,71
15
+ dlai_grader-2.0b1.dist-info/top_level.txt,sha256=4YKtA3ztisFtx_g4hsGivy3J2NHnXxFziIMqawC8HWg,12
16
+ dlai_grader-2.0b1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (78.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,16 +0,0 @@
1
- dlai_grader/__init__.py,sha256=FbFOZrZiR0vT6s9LdqnU3nNiuTZs2LbhLaF4OCJrDCY,211
2
- dlai_grader/cli.py,sha256=NIwboE-AFn1LXOFmF4O70Ow0fkRxgclG_eMwmWiua38,4917
3
- dlai_grader/compiler.py,sha256=elbHNUCqBCoOOoNmMRXbgeNL0nt0RM57eZi0-6AqycA,3036
4
- dlai_grader/config.py,sha256=HQ3dzaFpRswIA_7EC8XdP8DdJH-XePsbMQMHG8Esblc,1638
5
- dlai_grader/grading.py,sha256=Gmft9b7M8At_y_WZDatYdW6tinZMfqQoT7bDXp6uz2I,4606
6
- dlai_grader/io.py,sha256=TB9d01AK5FIbFUQwM8AqOOfuMWzjzrit98i3MhK5AqU,8234
7
- dlai_grader/notebook.py,sha256=noMU6DzPVylSjkHmSBUcmquVvAz4JigbRtbQrVYJdic,11830
8
- dlai_grader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- dlai_grader/templates.py,sha256=JDDPoFm13pqRIOwSpt5kXXtMUM7jOv2Uyz1ap8KWu4I,7850
10
- dlai_grader/types.py,sha256=_IIVbYL9cMmwA6in0aI5fEWCIaAMNcQbxG64X1P1CkE,335
11
- dlai_grader-1.22.0.dist-info/LICENSE,sha256=a_kch_UqdJPtyxk35QJr9O84K_koPixqWPYW9On4-io,1072
12
- dlai_grader-1.22.0.dist-info/METADATA,sha256=GWPfw91xdTsJfWcjOgcGmtmZ2bhM60KJAPHnmQc6oUQ,8807
13
- dlai_grader-1.22.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
14
- dlai_grader-1.22.0.dist-info/entry_points.txt,sha256=4OcSAUIluONXa3ymViQ7CBQ2Lk52nb6xZnfph1rlMnk,71
15
- dlai_grader-1.22.0.dist-info/top_level.txt,sha256=4YKtA3ztisFtx_g4hsGivy3J2NHnXxFziIMqawC8HWg,12
16
- dlai_grader-1.22.0.dist-info/RECORD,,