PyKubeGrader 0.2.12__tar.gz → 0.2.14__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/.gitignore +1 -0
- {pykubegrader-0.2.12/src/PyKubeGrader.egg-info → pykubegrader-0.2.14}/PKG-INFO +1 -1
- {pykubegrader-0.2.12 → pykubegrader-0.2.14/src/PyKubeGrader.egg-info}/PKG-INFO +1 -1
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/PyKubeGrader.egg-info/SOURCES.txt +1 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/build/build_folder.py +72 -45
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/initialize.py +4 -1
- pykubegrader-0.2.14/src/pykubegrader/tokens/tokens.py +53 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/.coveragerc +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/.github/workflows/main.yml +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/.readthedocs.yml +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/AUTHORS.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/CHANGELOG.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/CONTRIBUTING.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/LICENSE.txt +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/README.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/Makefile +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/_static/custom.css +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/authors.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/changelog.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/conf.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/contributing.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/index.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/license.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/readme.rst +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/docs/requirements.txt +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/examples/.responses.json +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/examples/true_false.ipynb +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/pyproject.toml +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/setup.cfg +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/setup.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/PyKubeGrader.egg-info/entry_points.txt +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/PyKubeGrader.egg-info/requires.txt +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/__init__.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/build/__init__.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/build/api_notebook_builder.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/build/clean_folder.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/graders/__init__.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/graders/late_assignments.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/log_parser/__init__.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/log_parser/parse.ipynb +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/log_parser/parse.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/submit/submit_assignment.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/telemetry.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/utils.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/validate.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/__init__.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/multiple_choice.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/reading_question.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/select_many.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/student_info.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/style.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/true_false.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets/types_question.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets_base/__init__.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets_base/multi_select.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets_base/reading.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/src/pykubegrader/widgets_base/select.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/tests/conftest.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/tests/import_test.py +0 -0
- {pykubegrader-0.2.12 → pykubegrader-0.2.14}/tox.ini +0 -0
@@ -47,6 +47,7 @@ src/pykubegrader/log_parser/__init__.py
|
|
47
47
|
src/pykubegrader/log_parser/parse.ipynb
|
48
48
|
src/pykubegrader/log_parser/parse.py
|
49
49
|
src/pykubegrader/submit/submit_assignment.py
|
50
|
+
src/pykubegrader/tokens/tokens.py
|
50
51
|
src/pykubegrader/widgets/__init__.py
|
51
52
|
src/pykubegrader/widgets/multiple_choice.py
|
52
53
|
src/pykubegrader/widgets/reading_question.py
|
@@ -1,7 +1,6 @@
|
|
1
1
|
### Note
|
2
2
|
|
3
3
|
|
4
|
-
|
5
4
|
import argparse
|
6
5
|
import importlib.util
|
7
6
|
import json
|
@@ -28,6 +27,7 @@ import nbformat
|
|
28
27
|
from .api_notebook_builder import FastAPINotebookBuilder
|
29
28
|
from typing import Optional
|
30
29
|
|
30
|
+
|
31
31
|
@dataclass
|
32
32
|
class NotebookProcessor:
|
33
33
|
"""
|
@@ -160,22 +160,21 @@ class NotebookProcessor:
|
|
160
160
|
|
161
161
|
if self.check_if_file_in_folder("assignment_config.yaml"):
|
162
162
|
self.add_assignment()
|
163
|
-
|
163
|
+
|
164
164
|
self.update_initialize_function()
|
165
|
-
|
165
|
+
|
166
166
|
def update_initialize_function(self):
|
167
|
-
|
167
|
+
|
168
168
|
for key, value in self.total_point_log.items():
|
169
|
-
|
169
|
+
|
170
170
|
assignment_tag = f"week{self.week_num}-{self.assignment_type}"
|
171
|
-
|
171
|
+
|
172
172
|
update_initialize_assignment(
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
173
|
+
notebook_path=os.path.join(self.root_folder, key + ".ipynb"),
|
174
|
+
assignment_points=value,
|
175
|
+
assignment_tag=assignment_tag,
|
176
|
+
)
|
177
|
+
|
179
178
|
def build_payload(self, yaml_content):
|
180
179
|
"""
|
181
180
|
Reads YAML content for an assignment and returns Python variables.
|
@@ -215,15 +214,15 @@ class NotebookProcessor:
|
|
215
214
|
"due_date": due_date,
|
216
215
|
"max_score": int(self.assignment_total_points),
|
217
216
|
}
|
218
|
-
|
219
|
-
def build_payload_notebook(self, yaml_content,
|
217
|
+
|
218
|
+
def build_payload_notebook(self, yaml_content, notebook_title, total_points):
|
220
219
|
# Parse the YAML content
|
221
220
|
with open(yaml_content, "r") as file:
|
222
221
|
data = yaml.safe_load(file)
|
223
|
-
|
222
|
+
|
224
223
|
# Extract assignment details
|
225
224
|
assignment = data.get("assignment", {})
|
226
|
-
|
225
|
+
|
227
226
|
week_num = self.week_num
|
228
227
|
assignment_type = self.assignment_type
|
229
228
|
due_date_str = assignment.get("due_date")
|
@@ -235,7 +234,7 @@ class NotebookProcessor:
|
|
235
234
|
due_date = parser.parse(due_date_str) # Automatically handles timezones
|
236
235
|
except ValueError as e:
|
237
236
|
print(f"Error parsing due_date: {e}")
|
238
|
-
|
237
|
+
|
239
238
|
return {
|
240
239
|
"title": notebook_title,
|
241
240
|
"week_number": week_num,
|
@@ -243,8 +242,7 @@ class NotebookProcessor:
|
|
243
242
|
"due_date": due_date,
|
244
243
|
"max_score": total_points,
|
245
244
|
}
|
246
|
-
|
247
|
-
|
245
|
+
|
248
246
|
def add_notebook(self, notebook_title, total_points):
|
249
247
|
"""
|
250
248
|
Sends a POST request to add a notebook.
|
@@ -253,9 +251,11 @@ class NotebookProcessor:
|
|
253
251
|
url = "https://engr-131-api.eastus.cloudapp.azure.com/notebook"
|
254
252
|
|
255
253
|
# Build the payload
|
256
|
-
payload = self.build_payload_notebook(
|
257
|
-
|
258
|
-
|
254
|
+
payload = self.build_payload_notebook(
|
255
|
+
yaml_content=f"{self.root_folder}/assignment_config.yaml",
|
256
|
+
notebook_title=notebook_title,
|
257
|
+
total_points=total_points,
|
258
|
+
)
|
259
259
|
|
260
260
|
# Define HTTP Basic Authentication
|
261
261
|
auth = (user(), password())
|
@@ -496,19 +496,50 @@ class NotebookProcessor:
|
|
496
496
|
self.select_many_total_points
|
497
497
|
+ self.mcq_total_points
|
498
498
|
+ self.tf_total_points
|
499
|
-
+ self.otter_total_points
|
499
|
+
+ self.otter_total_points
|
500
500
|
)
|
501
|
-
|
501
|
+
|
502
502
|
# creates the assignment record in the database
|
503
503
|
self.add_notebook(notebook_name, total_points)
|
504
504
|
|
505
505
|
self.assignment_total_points += total_points
|
506
506
|
|
507
507
|
self.total_point_log.update({notebook_name: total_points})
|
508
|
-
|
509
|
-
student_file_path = os.path.join(self.root_folder, notebook_name +
|
508
|
+
|
509
|
+
student_file_path = os.path.join(self.root_folder, notebook_name + ".ipynb")
|
510
510
|
self.add_submission_cells(student_file_path, student_file_path)
|
511
|
-
|
511
|
+
NotebookProcessor.remove_empty_cells(student_file_path)
|
512
|
+
|
513
|
+
@staticmethod
|
514
|
+
def remove_empty_cells(notebook_path, output_path=None):
|
515
|
+
"""
|
516
|
+
Removes empty cells from a Jupyter Notebook and saves the updated notebook.
|
517
|
+
|
518
|
+
Parameters:
|
519
|
+
notebook_path (str): Path to the input Jupyter Notebook.
|
520
|
+
output_path (str): Path to save the updated Jupyter Notebook. If None, it overwrites the original file.
|
521
|
+
"""
|
522
|
+
try:
|
523
|
+
# Load the notebook
|
524
|
+
with open(notebook_path, "r") as nb_file:
|
525
|
+
notebook = nbformat.read(nb_file, as_version=4)
|
526
|
+
|
527
|
+
# Filter out empty cells
|
528
|
+
non_empty_cells = [cell for cell in notebook.cells if cell.source.strip()]
|
529
|
+
|
530
|
+
# Update the notebook cells
|
531
|
+
notebook.cells = non_empty_cells
|
532
|
+
|
533
|
+
# Save the updated notebook
|
534
|
+
save_path = output_path if output_path else notebook_path
|
535
|
+
with open(save_path, "w") as nb_file:
|
536
|
+
nbformat.write(notebook, nb_file)
|
537
|
+
|
538
|
+
print(f"Empty cells removed. Updated notebook saved at: {save_path}")
|
539
|
+
|
540
|
+
except Exception as e:
|
541
|
+
print(f"An error occurred: {e}")
|
542
|
+
|
512
543
|
def add_submission_cells(self, notebook_path: str, output_path: str) -> None:
|
513
544
|
"""
|
514
545
|
Adds submission cells to the end of a Jupyter notebook.
|
@@ -533,12 +564,10 @@ class NotebookProcessor:
|
|
533
564
|
"from pykubegrader.submit.submit_assignment import submit_assignment\n\n"
|
534
565
|
f'submit_assignment("week{self.week_num}-{self.assignment_type}", "{os.path.basename(notebook_path).replace(".ipynb", "")}")'
|
535
566
|
)
|
536
|
-
|
567
|
+
|
537
568
|
# Make the code cell non-editable and non-deletable
|
538
|
-
code_cell.metadata = {
|
539
|
-
|
540
|
-
"deletable": False
|
541
|
-
}
|
569
|
+
code_cell.metadata = {"editable": False, "deletable": False}
|
570
|
+
code_cell.metadata["tags"] = ["skip-execution"]
|
542
571
|
|
543
572
|
# Add the cells to the notebook
|
544
573
|
notebook.cells.append(markdown_cell)
|
@@ -548,7 +577,6 @@ class NotebookProcessor:
|
|
548
577
|
with open(output_path, "w", encoding="utf-8") as f:
|
549
578
|
nbformat.write(notebook, f)
|
550
579
|
|
551
|
-
|
552
580
|
def free_response_parser(
|
553
581
|
self, temp_notebook_path, notebook_subfolder, notebook_name
|
554
582
|
):
|
@@ -783,7 +811,7 @@ class NotebookProcessor:
|
|
783
811
|
return solution_path, question_path
|
784
812
|
else:
|
785
813
|
return None, None
|
786
|
-
|
814
|
+
|
787
815
|
@staticmethod
|
788
816
|
def replace_temp_no_otter(input_file, output_file):
|
789
817
|
# Load the notebook
|
@@ -793,8 +821,8 @@ class NotebookProcessor:
|
|
793
821
|
# Iterate through the cells and modify `cell.source`
|
794
822
|
for cell in notebook.cells:
|
795
823
|
if cell.cell_type == "code": # Only process code cells
|
796
|
-
if
|
797
|
-
cell.source = cell.source.replace(
|
824
|
+
if "responses = initialize_assignment(" in cell.source:
|
825
|
+
cell.source = cell.source.replace("_temp", "")
|
798
826
|
|
799
827
|
# Save the modified notebook
|
800
828
|
with open(output_file, "w", encoding="utf-8") as f:
|
@@ -823,7 +851,6 @@ class NotebookProcessor:
|
|
823
851
|
cell["source"] = [
|
824
852
|
line.replace("_temp.ipynb", ".ipynb") for line in cell["source"]
|
825
853
|
]
|
826
|
-
|
827
854
|
|
828
855
|
# Write the updated notebook to the output file
|
829
856
|
with open(output_file, "w", encoding="utf-8") as f:
|
@@ -1508,6 +1535,7 @@ def extract_MCQ(ipynb_file):
|
|
1508
1535
|
print("Invalid JSON in notebook file.")
|
1509
1536
|
return []
|
1510
1537
|
|
1538
|
+
|
1511
1539
|
def check_for_heading(notebook_path, search_strings):
|
1512
1540
|
"""
|
1513
1541
|
Checks if a Jupyter notebook contains a heading cell whose source matches any of the given strings.
|
@@ -1547,8 +1575,6 @@ def clean_notebook(notebook_path):
|
|
1547
1575
|
cell.metadata["editable"] = cell.metadata.get("editable", False)
|
1548
1576
|
cell.metadata["deletable"] = cell.metadata.get("deletable", False)
|
1549
1577
|
if cell.cell_type == "code":
|
1550
|
-
if "grader.check(" in cell.source:
|
1551
|
-
continue
|
1552
1578
|
cell.metadata["tags"] = cell.metadata.get("tags", [])
|
1553
1579
|
if "skip-execution" not in cell.metadata["tags"]:
|
1554
1580
|
cell.metadata["tags"].append("skip-execution")
|
@@ -1785,7 +1811,7 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
|
|
1785
1811
|
)
|
1786
1812
|
f.write(" def __init__(self):\n")
|
1787
1813
|
f.write(" super().__init__(\n")
|
1788
|
-
f.write(f
|
1814
|
+
f.write(f' title=f"{q_value['question_text']}",\n')
|
1789
1815
|
f.write(" style=MultiSelect,\n")
|
1790
1816
|
f.write(
|
1791
1817
|
f" question_number={q_value['question number']},\n"
|
@@ -1862,7 +1888,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
|
|
1862
1888
|
)
|
1863
1889
|
f.write(" def __init__(self):\n")
|
1864
1890
|
f.write(" super().__init__(\n")
|
1865
|
-
f.write(f
|
1891
|
+
f.write(f' title=f"{q_value['question_text']}",\n')
|
1866
1892
|
f.write(" style=TFStyle,\n")
|
1867
1893
|
f.write(
|
1868
1894
|
f" question_number={q_value['question number']},\n"
|
@@ -1953,7 +1979,8 @@ def replace_cell_source(notebook_path, cell_index, new_source):
|
|
1953
1979
|
# Save the notebook
|
1954
1980
|
with open(notebook_path, "w", encoding="utf-8") as f:
|
1955
1981
|
nbformat.write(notebook, f)
|
1956
|
-
|
1982
|
+
|
1983
|
+
|
1957
1984
|
def update_initialize_assignment(
|
1958
1985
|
notebook_path: str,
|
1959
1986
|
assignment_points: Optional[float] = None,
|
@@ -2001,11 +2028,11 @@ def update_initialize_assignment(
|
|
2001
2028
|
existing_args = match.group(1).strip()
|
2002
2029
|
# Replace with the updated line
|
2003
2030
|
if additional_variables_str:
|
2031
|
+
updated_line = f"responses = initialize_assignment({existing_args}, {additional_variables_str})\n"
|
2032
|
+
else:
|
2004
2033
|
updated_line = (
|
2005
|
-
f"responses = initialize_assignment({existing_args}
|
2034
|
+
f"responses = initialize_assignment({existing_args})\n"
|
2006
2035
|
)
|
2007
|
-
else:
|
2008
|
-
updated_line = f"responses = initialize_assignment({existing_args})\n"
|
2009
2036
|
source_code[i] = updated_line
|
2010
2037
|
updated = True
|
2011
2038
|
|
@@ -9,7 +9,7 @@ from IPython import get_ipython
|
|
9
9
|
from typing import Optional
|
10
10
|
from .telemetry import ensure_responses, log_variable, telemetry, update_responses
|
11
11
|
|
12
|
-
|
12
|
+
#TODO: could cleanup to remove redundant imports
|
13
13
|
def initialize_assignment(
|
14
14
|
name: str,
|
15
15
|
week: str,
|
@@ -33,6 +33,9 @@ def initialize_assignment(
|
|
33
33
|
Raises:
|
34
34
|
Exception: If the environment is unsupported or initialization fails.
|
35
35
|
"""
|
36
|
+
|
37
|
+
if assignment_tag is None:
|
38
|
+
assignment_tag = f"{week}-{assignment_type}"
|
36
39
|
|
37
40
|
ipython = get_ipython()
|
38
41
|
if ipython is None:
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import requests
|
2
|
+
import os
|
3
|
+
import json
|
4
|
+
|
5
|
+
|
6
|
+
try:
|
7
|
+
from pykubegrader.build.passwords import password, user
|
8
|
+
except: # noqa: E722
|
9
|
+
print("Passwords not found, cannot access database")
|
10
|
+
|
11
|
+
|
12
|
+
def build_token_payload(token: str, duration: int) -> dict:
|
13
|
+
|
14
|
+
if os.getenv("JUPYTERHUB_USER", None) is None:
|
15
|
+
raise ValueError("JupyterHub user not found")
|
16
|
+
|
17
|
+
# Return the extracted details as a dictionary
|
18
|
+
return {
|
19
|
+
"value": token,
|
20
|
+
"requester": os.getenv("JUPYTERHUB_USER", None),
|
21
|
+
"duration": duration,
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
# Need to do for add token as well
|
26
|
+
def add_token(token, duration=20):
|
27
|
+
"""
|
28
|
+
Sends a POST request to add a notebook.
|
29
|
+
"""
|
30
|
+
# Define the URL
|
31
|
+
url = "https://engr-131-api.eastus.cloudapp.azure.com/tokens"
|
32
|
+
|
33
|
+
# Build the payload
|
34
|
+
payload = build_token_payload(token=token, duration=duration)
|
35
|
+
|
36
|
+
# Define HTTP Basic Authentication
|
37
|
+
auth = (user(), password())
|
38
|
+
|
39
|
+
# Define headers
|
40
|
+
headers = {"Content-Type": "application/json"}
|
41
|
+
|
42
|
+
# Serialize the payload with the custom JSON encoder
|
43
|
+
serialized_payload = json.dumps(payload)
|
44
|
+
|
45
|
+
# Send the POST request
|
46
|
+
response = requests.post(url, data=serialized_payload, headers=headers, auth=auth)
|
47
|
+
|
48
|
+
# Print the response
|
49
|
+
print(f"Status Code: {response.status_code}")
|
50
|
+
try:
|
51
|
+
print(f"Response: {response.json()}")
|
52
|
+
except ValueError:
|
53
|
+
print(f"Response: {response.text}")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|