PyKubeGrader 0.3.2__tar.gz → 0.3.4__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.
- {pykubegrader-0.3.2/src/PyKubeGrader.egg-info → pykubegrader-0.3.4}/PKG-INFO +1 -4
- pykubegrader-0.3.4/ruff.toml +2 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/setup.cfg +1 -3
- {pykubegrader-0.3.2 → pykubegrader-0.3.4/src/PyKubeGrader.egg-info}/PKG-INFO +1 -4
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/PyKubeGrader.egg-info/SOURCES.txt +2 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/PyKubeGrader.egg-info/entry_points.txt +1 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/PyKubeGrader.egg-info/requires.txt +0 -3
- pykubegrader-0.3.4/src/pykubegrader/build/collate.py +190 -0
- pykubegrader-0.3.4/src/pykubegrader/grade_reports/grade_reports.py +76 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/grading_tester.ipynb +47 -36
- pykubegrader-0.3.4/src/pykubegrader/submit/submit_assignment.py +88 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/telemetry.py +196 -1
- pykubegrader-0.3.4/test.ipynb +50 -0
- pykubegrader-0.3.2/ruff.toml +0 -2
- pykubegrader-0.3.2/src/pykubegrader/grade_reports/grade_reports.py +0 -171
- pykubegrader-0.3.2/src/pykubegrader/submit/submit_assignment.py +0 -104
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/.coveragerc +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/.github/workflows/main.yml +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/.gitignore +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/.readthedocs.yml +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/AUTHORS.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/CHANGELOG.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/CONTRIBUTING.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/LICENSE.txt +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/README.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/Makefile +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/_static/custom.css +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/authors.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/changelog.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/conf.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/contributing.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/index.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/license.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/readme.rst +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/docs/requirements.txt +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/examples/.responses.json +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/examples/true_false.ipynb +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/pyproject.toml +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/setup.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/__init__.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/build/__init__.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/build/api_notebook_builder.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/build/build_folder.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/build/clean_folder.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/build/markdown_questions.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/graders/__init__.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/graders/late_assignments.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/initialize.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/log_parser/__init__.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/log_parser/parse.ipynb +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/log_parser/parse.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/tokens/tokens.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/tokens/validate_token.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/utils.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/validate.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/__init__.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/multiple_choice.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/question_processor.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/reading_question.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/select_many.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/student_info.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/style.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/true_false.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets/types_question.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets_base/__init__.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets_base/multi_select.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets_base/reading.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/src/pykubegrader/widgets_base/select.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/tests/conftest.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/tests/import_test.py +0 -0
- {pykubegrader-0.3.2 → pykubegrader-0.3.4}/tox.ini +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyKubeGrader
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.4
|
4
4
|
Summary: Add a short description here!
|
5
5
|
Home-page: https://github.com/pyscaffold/pyscaffold/
|
6
6
|
Author: jagar2
|
@@ -12,12 +12,10 @@ Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Programming Language :: Python
|
13
13
|
Description-Content-Type: text/x-rst; charset=UTF-8
|
14
14
|
License-File: LICENSE.txt
|
15
|
-
Requires-Dist: httpx
|
16
15
|
Requires-Dist: importlib-metadata; python_version < "3.8"
|
17
16
|
Requires-Dist: ipython
|
18
17
|
Requires-Dist: mypy
|
19
18
|
Requires-Dist: nbformat
|
20
|
-
Requires-Dist: nest_asyncio
|
21
19
|
Requires-Dist: numpy
|
22
20
|
Requires-Dist: pandas-stubs
|
23
21
|
Requires-Dist: panel
|
@@ -28,7 +26,6 @@ Requires-Dist: ruff
|
|
28
26
|
Requires-Dist: setuptools
|
29
27
|
Requires-Dist: sphinx
|
30
28
|
Requires-Dist: types-python-dateutil
|
31
|
-
Requires-Dist: types-pyyaml
|
32
29
|
Requires-Dist: types-requests
|
33
30
|
Requires-Dist: types-setuptools
|
34
31
|
Provides-Extra: testing
|
@@ -22,12 +22,10 @@ include_package_data = True
|
|
22
22
|
package_dir =
|
23
23
|
=src
|
24
24
|
install_requires =
|
25
|
-
httpx
|
26
25
|
importlib-metadata; python_version<"3.8"
|
27
26
|
ipython
|
28
27
|
mypy
|
29
28
|
nbformat
|
30
|
-
nest_asyncio
|
31
29
|
numpy
|
32
30
|
pandas-stubs
|
33
31
|
panel
|
@@ -38,7 +36,6 @@ install_requires =
|
|
38
36
|
setuptools
|
39
37
|
sphinx
|
40
38
|
types-python-dateutil
|
41
|
-
types-pyyaml
|
42
39
|
types-requests
|
43
40
|
types-setuptools
|
44
41
|
|
@@ -59,6 +56,7 @@ console_scripts =
|
|
59
56
|
otter-folder-builder = pykubegrader.build.build_folder:main
|
60
57
|
otter-folder-cleaner = pykubegrader.build.clean_folder:main
|
61
58
|
markdown-question = pykubegrader.build.markdown_questions:main
|
59
|
+
collate-questions = pykubegrader.build.collate:main
|
62
60
|
|
63
61
|
[tool:pytest]
|
64
62
|
addopts =
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: PyKubeGrader
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.4
|
4
4
|
Summary: Add a short description here!
|
5
5
|
Home-page: https://github.com/pyscaffold/pyscaffold/
|
6
6
|
Author: jagar2
|
@@ -12,12 +12,10 @@ Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Programming Language :: Python
|
13
13
|
Description-Content-Type: text/x-rst; charset=UTF-8
|
14
14
|
License-File: LICENSE.txt
|
15
|
-
Requires-Dist: httpx
|
16
15
|
Requires-Dist: importlib-metadata; python_version < "3.8"
|
17
16
|
Requires-Dist: ipython
|
18
17
|
Requires-Dist: mypy
|
19
18
|
Requires-Dist: nbformat
|
20
|
-
Requires-Dist: nest_asyncio
|
21
19
|
Requires-Dist: numpy
|
22
20
|
Requires-Dist: pandas-stubs
|
23
21
|
Requires-Dist: panel
|
@@ -28,7 +26,6 @@ Requires-Dist: ruff
|
|
28
26
|
Requires-Dist: setuptools
|
29
27
|
Requires-Dist: sphinx
|
30
28
|
Requires-Dist: types-python-dateutil
|
31
|
-
Requires-Dist: types-pyyaml
|
32
29
|
Requires-Dist: types-requests
|
33
30
|
Requires-Dist: types-setuptools
|
34
31
|
Provides-Extra: testing
|
@@ -10,6 +10,7 @@ pyproject.toml
|
|
10
10
|
ruff.toml
|
11
11
|
setup.cfg
|
12
12
|
setup.py
|
13
|
+
test.ipynb
|
13
14
|
tox.ini
|
14
15
|
.github/workflows/main.yml
|
15
16
|
docs/Makefile
|
@@ -43,6 +44,7 @@ src/pykubegrader/build/__init__.py
|
|
43
44
|
src/pykubegrader/build/api_notebook_builder.py
|
44
45
|
src/pykubegrader/build/build_folder.py
|
45
46
|
src/pykubegrader/build/clean_folder.py
|
47
|
+
src/pykubegrader/build/collate.py
|
46
48
|
src/pykubegrader/build/markdown_questions.py
|
47
49
|
src/pykubegrader/grade_reports/grade_reports.py
|
48
50
|
src/pykubegrader/graders/__init__.py
|
@@ -0,0 +1,190 @@
|
|
1
|
+
import argparse
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
|
5
|
+
from nbformat.v4 import new_markdown_cell, new_notebook
|
6
|
+
|
7
|
+
|
8
|
+
class QuestionCollator:
|
9
|
+
def __init__(self, root_folder: str, output_path: str):
|
10
|
+
"""
|
11
|
+
Initializes the QuestionCollator with the root folder and output path.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
root_folder (str): Path to the root folder containing the solution files.
|
15
|
+
output_path (str): Path to save the collated notebook.
|
16
|
+
"""
|
17
|
+
self.root_folder = root_folder
|
18
|
+
self.output_path = output_path
|
19
|
+
|
20
|
+
def find_solution_folders(self):
|
21
|
+
"""
|
22
|
+
Finds all immediate subdirectories inside '_solution*' folders that contain notebooks.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
list: List of folder paths containing notebooks.
|
26
|
+
"""
|
27
|
+
solution_folders = []
|
28
|
+
|
29
|
+
# Look for _solution* folders inside the root_folder
|
30
|
+
for dir_name in os.listdir(self.root_folder):
|
31
|
+
solution_folder_path = os.path.join(self.root_folder, dir_name)
|
32
|
+
|
33
|
+
if os.path.isdir(solution_folder_path) and dir_name.startswith("_solution"):
|
34
|
+
print(f"Found solution folder: {solution_folder_path}") # Debug output
|
35
|
+
|
36
|
+
# Now, look for immediate subdirectories inside this _solution* folder
|
37
|
+
for sub_dir in os.listdir(solution_folder_path):
|
38
|
+
sub_dir_path = os.path.join(solution_folder_path, sub_dir)
|
39
|
+
|
40
|
+
if os.path.isdir(sub_dir_path):
|
41
|
+
# Check if this subdirectory contains at least one .ipynb file
|
42
|
+
if any(f.endswith(".ipynb") for f in os.listdir(sub_dir_path)):
|
43
|
+
solution_folders.append(sub_dir_path)
|
44
|
+
|
45
|
+
print(f"Final list of solution subfolders: {solution_folders}") # Debug output
|
46
|
+
return solution_folders
|
47
|
+
|
48
|
+
def extract_questions(self, folder_path):
|
49
|
+
"""
|
50
|
+
Extracts questions from all notebooks in the solution folder.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
folder_path (str): Path to the solution folder.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
dict: Dictionary of categorized questions.
|
57
|
+
"""
|
58
|
+
questions = {
|
59
|
+
"multiple_choice": [],
|
60
|
+
"select_many": [],
|
61
|
+
"true_false": [],
|
62
|
+
"other": [],
|
63
|
+
}
|
64
|
+
|
65
|
+
for file in os.listdir(folder_path):
|
66
|
+
if file.endswith(".ipynb"):
|
67
|
+
file_path = os.path.join(folder_path, file)
|
68
|
+
print(
|
69
|
+
f"Processing notebook: {file_path}"
|
70
|
+
) # Print the full path of the notebook
|
71
|
+
with open(file_path, "r") as f:
|
72
|
+
content = json.load(f)
|
73
|
+
|
74
|
+
# Track whether we are inside a question block
|
75
|
+
in_question_block = False
|
76
|
+
current_question_content = []
|
77
|
+
|
78
|
+
for cell in content["cells"]:
|
79
|
+
if "# BEGIN MULTIPLE CHOICE" in cell["source"]:
|
80
|
+
# Start of a question block
|
81
|
+
in_question_block = True
|
82
|
+
current_question_content = []
|
83
|
+
elif "# END MULTIPLE CHOICE" in cell["source"]:
|
84
|
+
# End of a question block
|
85
|
+
in_question_block = False
|
86
|
+
if current_question_content:
|
87
|
+
questions["multiple_choice"].append(
|
88
|
+
{"source": "\n".join(current_question_content)}
|
89
|
+
)
|
90
|
+
current_question_content = []
|
91
|
+
elif in_question_block and cell["cell_type"] == "markdown":
|
92
|
+
# Capture markdown cells within the question block
|
93
|
+
current_question_content.append(cell["source"])
|
94
|
+
|
95
|
+
return questions
|
96
|
+
|
97
|
+
def create_collated_notebook(self, questions):
|
98
|
+
"""
|
99
|
+
Creates a new notebook with questions organized by type.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
questions (dict): Dictionary of categorized questions.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Notebook: The collated notebook.
|
106
|
+
"""
|
107
|
+
nb = new_notebook()
|
108
|
+
|
109
|
+
# Add Multiple Choice Questions
|
110
|
+
nb.cells.append(new_markdown_cell("# Multiple Choice Questions"))
|
111
|
+
for q in questions["multiple_choice"]:
|
112
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
113
|
+
|
114
|
+
# Add Select Many Questions
|
115
|
+
nb.cells.append(new_markdown_cell("# Select Many Questions"))
|
116
|
+
for q in questions["select_many"]:
|
117
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
118
|
+
|
119
|
+
# Add True/False Questions
|
120
|
+
nb.cells.append(new_markdown_cell("# True/False Questions"))
|
121
|
+
for q in questions["true_false"]:
|
122
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
123
|
+
|
124
|
+
# Add Other Questions
|
125
|
+
nb.cells.append(new_markdown_cell("# Other Questions"))
|
126
|
+
for q in questions["other"]:
|
127
|
+
nb.cells.append(new_markdown_cell(q["source"]))
|
128
|
+
|
129
|
+
return nb
|
130
|
+
|
131
|
+
def save_notebook(self, nb):
|
132
|
+
"""
|
133
|
+
Saves the collated notebook to the specified output path.
|
134
|
+
|
135
|
+
Args:
|
136
|
+
nb (Notebook): The notebook to save.
|
137
|
+
"""
|
138
|
+
import nbformat
|
139
|
+
|
140
|
+
with open(self.output_path, "w") as f:
|
141
|
+
nbformat.write(nb, f)
|
142
|
+
|
143
|
+
def collate_questions(self):
|
144
|
+
"""
|
145
|
+
Collates questions from all solution folders and saves them to a new notebook.
|
146
|
+
"""
|
147
|
+
solution_folders = self.find_solution_folders()
|
148
|
+
all_questions = {
|
149
|
+
"multiple_choice": [],
|
150
|
+
"select_many": [],
|
151
|
+
"true_false": [],
|
152
|
+
"other": [],
|
153
|
+
}
|
154
|
+
|
155
|
+
for folder in solution_folders:
|
156
|
+
questions = self.extract_questions(folder)
|
157
|
+
all_questions["multiple_choice"].extend(questions["multiple_choice"])
|
158
|
+
all_questions["select_many"].extend(questions["select_many"])
|
159
|
+
all_questions["true_false"].extend(questions["true_false"])
|
160
|
+
all_questions["other"].extend(questions["other"])
|
161
|
+
|
162
|
+
collated_nb = self.create_collated_notebook(all_questions)
|
163
|
+
self.save_notebook(collated_nb)
|
164
|
+
print(f"Collated notebook saved to {self.output_path}")
|
165
|
+
|
166
|
+
|
167
|
+
def main():
|
168
|
+
parser = argparse.ArgumentParser(
|
169
|
+
description="Collate questions from solution folders into a single notebook."
|
170
|
+
)
|
171
|
+
parser.add_argument(
|
172
|
+
"root_folder",
|
173
|
+
type=str,
|
174
|
+
help="Path to the root folder containing solution folders",
|
175
|
+
)
|
176
|
+
parser.add_argument(
|
177
|
+
"output_path", type=str, help="Path to save the collated notebook"
|
178
|
+
)
|
179
|
+
|
180
|
+
args = parser.parse_args()
|
181
|
+
collator = QuestionCollator(
|
182
|
+
root_folder=args.root_folder, output_path=args.output_path
|
183
|
+
)
|
184
|
+
collator.collate_questions()
|
185
|
+
|
186
|
+
|
187
|
+
if __name__ == "__main__":
|
188
|
+
import sys
|
189
|
+
|
190
|
+
sys.exit(main())
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import pandas as pd
|
4
|
+
import requests
|
5
|
+
from requests.auth import HTTPBasicAuth
|
6
|
+
|
7
|
+
|
8
|
+
def format_assignment_table(assignments):
|
9
|
+
# Create DataFrame
|
10
|
+
df = pd.DataFrame(assignments)
|
11
|
+
|
12
|
+
# Replacements for normalization
|
13
|
+
replacements = {
|
14
|
+
"practicequiz": "practice quiz",
|
15
|
+
"practice-quiz": "practice quiz",
|
16
|
+
"attend": "attendance",
|
17
|
+
"attendance": "attendance",
|
18
|
+
}
|
19
|
+
|
20
|
+
# Remove assignments of type 'test'
|
21
|
+
remove_assignments = ["test"]
|
22
|
+
|
23
|
+
# Apply replacements
|
24
|
+
df["assignment_name"] = df["assignment_type"].replace(replacements)
|
25
|
+
|
26
|
+
# Filter out specific assignment types
|
27
|
+
df = df[~df["assignment_type"].isin(remove_assignments)]
|
28
|
+
|
29
|
+
# Sort by week number and assignment name
|
30
|
+
df = df.sort_values(by=["assignment_name", "week_number"]).reset_index(drop=True)
|
31
|
+
|
32
|
+
return df
|
33
|
+
|
34
|
+
|
35
|
+
def get_student_grades(student_username):
|
36
|
+
# Get env variables here, in the function, rather than globally
|
37
|
+
api_base_url = os.getenv("DB_URL")
|
38
|
+
student_user = os.getenv("user_name_student")
|
39
|
+
student_pw = os.getenv("keys_student")
|
40
|
+
|
41
|
+
if not api_base_url or not student_user or not student_pw:
|
42
|
+
raise ValueError("Environment variables not set")
|
43
|
+
|
44
|
+
params = {"username": student_username}
|
45
|
+
res = requests.get(
|
46
|
+
url=api_base_url.rstrip("/") + "/student-grades-testing",
|
47
|
+
params=params,
|
48
|
+
auth=HTTPBasicAuth(student_user, student_pw),
|
49
|
+
)
|
50
|
+
[assignments, sub] = res.json()
|
51
|
+
|
52
|
+
assignments_df = format_assignment_table(assignments)
|
53
|
+
|
54
|
+
return assignments_df, pd.DataFrame(sub)
|
55
|
+
|
56
|
+
|
57
|
+
def filter_assignments(df, max_week=None, exclude_types=None):
|
58
|
+
"""
|
59
|
+
Remove assignments with week_number greater than max_week
|
60
|
+
or with specific assignment types.
|
61
|
+
|
62
|
+
:param df: DataFrame containing assignments.
|
63
|
+
:param max_week: Maximum allowed week_number (int).
|
64
|
+
:param exclude_types: A single assignment type or a list of assignment types to exclude.
|
65
|
+
:return: Filtered DataFrame.
|
66
|
+
"""
|
67
|
+
if max_week is not None:
|
68
|
+
df = df[df["week_number"] <= max_week]
|
69
|
+
|
70
|
+
if exclude_types is not None:
|
71
|
+
# Ensure exclude_types is a list
|
72
|
+
if not isinstance(exclude_types, (list, tuple, set)):
|
73
|
+
exclude_types = [exclude_types]
|
74
|
+
df = df[~df["assignment_type"].isin(exclude_types)]
|
75
|
+
|
76
|
+
return df
|
@@ -1,5 +1,16 @@
|
|
1
1
|
{
|
2
2
|
"cells": [
|
3
|
+
{
|
4
|
+
"cell_type": "code",
|
5
|
+
"execution_count": null,
|
6
|
+
"metadata": {},
|
7
|
+
"outputs": [],
|
8
|
+
"source": [
|
9
|
+
"from grade_reports.grade_reports import filter_assignments, get_student_grades\n",
|
10
|
+
"\n",
|
11
|
+
"get_student_grades"
|
12
|
+
]
|
13
|
+
},
|
3
14
|
{
|
4
15
|
"cell_type": "code",
|
5
16
|
"execution_count": null,
|
@@ -19,13 +30,13 @@
|
|
19
30
|
"api_base_url = \"https://engr-131-api.eastus.cloudapp.azure.com/\"\n",
|
20
31
|
"\n",
|
21
32
|
"\n",
|
22
|
-
"def get_all_students():\n",
|
23
|
-
"
|
24
|
-
"
|
25
|
-
"
|
26
|
-
"
|
33
|
+
"# def get_all_students():\n",
|
34
|
+
"# res = requests.get(\n",
|
35
|
+
"# url=api_base_url.rstrip(\"/\") + \"/get-all-submission-emails\",\n",
|
36
|
+
"# auth=HTTPBasicAuth(user(), password()),\n",
|
37
|
+
"# )\n",
|
27
38
|
"\n",
|
28
|
-
"
|
39
|
+
"# return res.json()\n",
|
29
40
|
"\n",
|
30
41
|
"\n",
|
31
42
|
"# def get_student_grades(student_id):\n",
|
@@ -41,47 +52,47 @@
|
|
41
52
|
"# return assignments, sub\n",
|
42
53
|
"\n",
|
43
54
|
"\n",
|
44
|
-
"def get_student(student):\n",
|
45
|
-
"
|
46
|
-
"
|
47
|
-
"
|
55
|
+
"# def get_student(student):\n",
|
56
|
+
"# print(student)\n",
|
57
|
+
"# # Get assignments and submissions for the student (assumed functions)\n",
|
58
|
+
"# assignments, submissions = get_student_grades(student)\n",
|
48
59
|
"\n",
|
49
|
-
"
|
50
|
-
"
|
60
|
+
"# # Recalculate grades and get a grades dictionary\n",
|
61
|
+
"# grades_dict = recalculate_best_grades(assignments, submissions)\n",
|
51
62
|
"\n",
|
52
|
-
"
|
53
|
-
"
|
63
|
+
"# # Calculate averages and build a row for the student\n",
|
64
|
+
"# row = calculate_averages(grades_dict, student)\n",
|
54
65
|
"\n",
|
55
|
-
"
|
56
|
-
"
|
66
|
+
"# # Convert the row (a dictionary) into a DataFrame\n",
|
67
|
+
"# # row_df = pd.DataFrame([row])\n",
|
57
68
|
"\n",
|
58
|
-
"
|
69
|
+
"# return row\n",
|
59
70
|
"\n",
|
60
71
|
"\n",
|
61
|
-
"def get_all_student_grades():\n",
|
62
|
-
"
|
63
|
-
"
|
72
|
+
"# def get_all_student_grades():\n",
|
73
|
+
"# # Initialize an empty DataFrame to hold all student grades\n",
|
74
|
+
"# df = pd.DataFrame()\n",
|
64
75
|
"\n",
|
65
|
-
"
|
66
|
-
"
|
76
|
+
"# # Get all students (assuming get_all_students() is a defined function)\n",
|
77
|
+
"# students = get_all_students()\n",
|
67
78
|
"\n",
|
68
|
-
"
|
69
|
-
"
|
79
|
+
"# for student in students:\n",
|
80
|
+
"# row_df = get_student(student)\n",
|
70
81
|
"\n",
|
71
|
-
"
|
72
|
-
"
|
82
|
+
"# # Append the row to the DataFrame\n",
|
83
|
+
"# df = pd.concat([df, row_df], ignore_index=True)\n",
|
73
84
|
"\n",
|
74
|
-
"
|
85
|
+
"# return df\n",
|
75
86
|
"\n",
|
76
87
|
"\n",
|
77
|
-
"def get_max_deadline(assignments, assignment_name, week_number):\n",
|
78
|
-
"
|
79
|
-
"
|
80
|
-
"
|
81
|
-
"
|
88
|
+
"# def get_max_deadline(assignments, assignment_name, week_number):\n",
|
89
|
+
"# matching_rows = assignments[\n",
|
90
|
+
"# (assignments[\"week_number\"] == week_number)\n",
|
91
|
+
"# & (assignments[\"assignment_name\"] == assignment_name)\n",
|
92
|
+
"# ]\n",
|
82
93
|
"\n",
|
83
|
-
"
|
84
|
-
"
|
94
|
+
"# max_timestamp = matching_rows[\"due_date\"].max()\n",
|
95
|
+
"# return max_timestamp\n",
|
85
96
|
"\n",
|
86
97
|
"\n",
|
87
98
|
"def calculate_averages(grades_dict, student_id):\n",
|
@@ -455,7 +466,7 @@
|
|
455
466
|
],
|
456
467
|
"metadata": {
|
457
468
|
"kernelspec": {
|
458
|
-
"display_name": "
|
469
|
+
"display_name": "engr131w25",
|
459
470
|
"language": "python",
|
460
471
|
"name": "python3"
|
461
472
|
},
|
@@ -469,7 +480,7 @@
|
|
469
480
|
"name": "python",
|
470
481
|
"nbconvert_exporter": "python",
|
471
482
|
"pygments_lexer": "ipython3",
|
472
|
-
"version": "3.
|
483
|
+
"version": "3.13.1"
|
473
484
|
}
|
474
485
|
},
|
475
486
|
"nbformat": 4,
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from requests.auth import HTTPBasicAuth
|
5
|
+
|
6
|
+
|
7
|
+
def get_credentials():
|
8
|
+
"""
|
9
|
+
Fetch the username and password from environment variables.
|
10
|
+
"""
|
11
|
+
username = os.getenv("user_name_student")
|
12
|
+
password = os.getenv("keys_student")
|
13
|
+
if not username or not password:
|
14
|
+
raise ValueError(
|
15
|
+
"Environment variables 'user_name_student' or 'keys_student' are not set."
|
16
|
+
)
|
17
|
+
return {"username": username, "password": password}
|
18
|
+
|
19
|
+
|
20
|
+
def call_score_assignment(
|
21
|
+
assignment_title: str, notebook_title: str, file_path: str = ".output_reduced.log"
|
22
|
+
) -> dict[str, str]:
|
23
|
+
"""
|
24
|
+
Submit an assignment to the scoring endpoint
|
25
|
+
|
26
|
+
Args:
|
27
|
+
assignment_title (str): Title of the assignment
|
28
|
+
notebook_title (str): Title of the notebook
|
29
|
+
file_path (str): Path to the log file to upload
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
dict: JSON response from the server
|
33
|
+
"""
|
34
|
+
|
35
|
+
base_url = os.getenv("DB_URL")
|
36
|
+
if not base_url:
|
37
|
+
raise ValueError("Environment variable 'DB_URL' not set")
|
38
|
+
|
39
|
+
url = base_url.rstrip("/") + "/score-assignment"
|
40
|
+
|
41
|
+
params = {
|
42
|
+
"assignment_title": assignment_title,
|
43
|
+
"notebook_title": notebook_title,
|
44
|
+
}
|
45
|
+
|
46
|
+
username, password = get_credentials().values()
|
47
|
+
|
48
|
+
try:
|
49
|
+
with open(file_path, "rb") as file:
|
50
|
+
res = requests.post(
|
51
|
+
url=url,
|
52
|
+
params=params,
|
53
|
+
auth=HTTPBasicAuth(username, password),
|
54
|
+
files={"log_file": file},
|
55
|
+
)
|
56
|
+
res.raise_for_status()
|
57
|
+
|
58
|
+
return res.json()
|
59
|
+
|
60
|
+
except FileNotFoundError:
|
61
|
+
raise FileNotFoundError(f"File {file_path} does not exist")
|
62
|
+
except requests.RequestException as err:
|
63
|
+
raise RuntimeError(f"An error occurred while requesting {url}: {err}")
|
64
|
+
except Exception as err:
|
65
|
+
raise RuntimeError(f"An unexpected error occurred: {err}")
|
66
|
+
|
67
|
+
|
68
|
+
def submit_assignment(
|
69
|
+
assignment_title: str,
|
70
|
+
notebook_title: str,
|
71
|
+
file_path: str = ".output_reduced.log",
|
72
|
+
) -> None:
|
73
|
+
"""
|
74
|
+
Synchronous wrapper for the `call_score_assignment` function.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
assignment_title (str): Title of the assignment.
|
78
|
+
file_path (str): Path to the log file to upload.
|
79
|
+
"""
|
80
|
+
|
81
|
+
response = call_score_assignment(assignment_title, notebook_title, file_path)
|
82
|
+
|
83
|
+
print("Server Response:", response.get("message", "No message in response"))
|
84
|
+
|
85
|
+
|
86
|
+
# Example usage (remove this section if only the function needs to be importable):
|
87
|
+
if __name__ == "__main__":
|
88
|
+
submit_assignment("week1-readings", "path/to/your/log_file.txt")
|