PyKubeGrader 0.1.4__tar.gz → 0.1.5__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {pykubegrader-0.1.4/src/PyKubeGrader.egg-info → pykubegrader-0.1.5}/PKG-INFO +2 -1
- pykubegrader-0.1.5/examples/.responses.json +1 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/examples/true_false.ipynb +1 -6
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/setup.cfg +1 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5/src/PyKubeGrader.egg-info}/PKG-INFO +2 -1
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/PyKubeGrader.egg-info/requires.txt +1 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/build/build_folder.py +31 -70
- pykubegrader-0.1.5/src/pykubegrader/widgets/__init__.py +19 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets/style.py +0 -2
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets/true_false.py +3 -10
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets_base/select.py +2 -3
- pykubegrader-0.1.4/examples/.responses.json +0 -1
- pykubegrader-0.1.4/src/pykubegrader/widgets/__init__.py +0 -10
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/.coveragerc +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/.github/workflows/main.yml +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/.gitignore +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/.readthedocs.yml +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/AUTHORS.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/CHANGELOG.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/CONTRIBUTING.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/LICENSE.txt +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/README.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/Makefile +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/_static/custom.css +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/authors.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/changelog.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/conf.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/contributing.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/index.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/license.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/readme.rst +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/docs/requirements.txt +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/pyproject.toml +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/setup.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/PyKubeGrader.egg-info/SOURCES.txt +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/PyKubeGrader.egg-info/entry_points.txt +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/__init__.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/initialize.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/telemetry.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/utils.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/validate.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets/multiple_choice.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets/reading_question.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets/select_many.py +1 -1
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets/student_info.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets/types_question.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets_base/__init__.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets_base/multi_select.py +1 -1
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/src/pykubegrader/widgets_base/reading.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/tests/conftest.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/tests/import_test.py +0 -0
- {pykubegrader-0.1.4 → pykubegrader-0.1.5}/tox.ini +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyKubeGrader
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
4
4
|
Summary: Add a short description here!
|
5
5
|
Home-page: https://github.com/pyscaffold/pyscaffold/
|
6
6
|
Author: jagar2
|
@@ -15,6 +15,7 @@ License-File: LICENSE.txt
|
|
15
15
|
Requires-Dist: importlib-metadata; python_version < "3.8"
|
16
16
|
Requires-Dist: ipython
|
17
17
|
Requires-Dist: mypy
|
18
|
+
Requires-Dist: nbformat
|
18
19
|
Requires-Dist: numpy
|
19
20
|
Requires-Dist: panel
|
20
21
|
Requires-Dist: pynacl
|
@@ -0,0 +1 @@
|
|
1
|
+
{ "seed": 30, "MC1": "True", "MC2": "True", "MC3": "False", "MC4": "False" }
|
@@ -16,7 +16,6 @@
|
|
16
16
|
"outputs": [],
|
17
17
|
"source": [
|
18
18
|
"class Question1(TFQuestion):\n",
|
19
|
-
"\n",
|
20
19
|
" def __init__(\n",
|
21
20
|
" self,\n",
|
22
21
|
" title=\"Respond with True or False\",\n",
|
@@ -56,11 +55,7 @@
|
|
56
55
|
"execution_count": null,
|
57
56
|
"metadata": {},
|
58
57
|
"outputs": [],
|
59
|
-
"source": [
|
60
|
-
"import time\n",
|
61
|
-
"from IPython.display import display, update_display\n",
|
62
|
-
"\n"
|
63
|
-
]
|
58
|
+
"source": []
|
64
59
|
},
|
65
60
|
{
|
66
61
|
"cell_type": "code",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: PyKubeGrader
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
4
4
|
Summary: Add a short description here!
|
5
5
|
Home-page: https://github.com/pyscaffold/pyscaffold/
|
6
6
|
Author: jagar2
|
@@ -15,6 +15,7 @@ License-File: LICENSE.txt
|
|
15
15
|
Requires-Dist: importlib-metadata; python_version < "3.8"
|
16
16
|
Requires-Dist: ipython
|
17
17
|
Requires-Dist: mypy
|
18
|
+
Requires-Dist: nbformat
|
18
19
|
Requires-Dist: numpy
|
19
20
|
Requires-Dist: panel
|
20
21
|
Requires-Dist: pynacl
|
@@ -1,14 +1,15 @@
|
|
1
|
-
|
1
|
+
import argparse
|
2
|
+
import importlib.util
|
3
|
+
import json
|
4
|
+
import logging
|
2
5
|
import os
|
6
|
+
import re
|
3
7
|
import shutil
|
4
|
-
import nbformat
|
5
8
|
import subprocess
|
6
9
|
import sys
|
7
|
-
import
|
8
|
-
|
9
|
-
import
|
10
|
-
import re
|
11
|
-
import importlib.util
|
10
|
+
from dataclasses import dataclass, field
|
11
|
+
|
12
|
+
import nbformat
|
12
13
|
|
13
14
|
|
14
15
|
@dataclass
|
@@ -182,11 +183,11 @@ class NotebookProcessor:
|
|
182
183
|
shutil.copy(notebook_path, temp_notebook_path)
|
183
184
|
|
184
185
|
# Determine the path to the autograder folder
|
185
|
-
autograder_path = os.path.join(notebook_subfolder,
|
186
|
+
autograder_path = os.path.join(notebook_subfolder, "dist/autograder/")
|
186
187
|
os.makedirs(autograder_path, exist_ok=True)
|
187
188
|
|
188
189
|
# Determine the path to the student folder
|
189
|
-
student_path = os.path.join(notebook_subfolder,
|
190
|
+
student_path = os.path.join(notebook_subfolder, "dist/student/")
|
190
191
|
os.makedirs(student_path, exist_ok=True)
|
191
192
|
|
192
193
|
if os.path.abspath(notebook_path) != os.path.abspath(new_notebook_path):
|
@@ -230,7 +231,6 @@ class NotebookProcessor:
|
|
230
231
|
|
231
232
|
### Parse the notebook for TF questions
|
232
233
|
if self.has_assignment(temp_notebook_path, "# BEGIN TF"):
|
233
|
-
|
234
234
|
markers = ("# BEGIN TF", "# END TF")
|
235
235
|
|
236
236
|
self._print_and_log(
|
@@ -262,7 +262,6 @@ class NotebookProcessor:
|
|
262
262
|
|
263
263
|
### Parse the notebook for select_many questions
|
264
264
|
if self.has_assignment(temp_notebook_path, "# BEGIN SELECT MANY"):
|
265
|
-
|
266
265
|
markers = ("# BEGIN SELECT MANY", "# END SELECT MANY")
|
267
266
|
|
268
267
|
self._print_and_log(
|
@@ -347,23 +346,31 @@ class NotebookProcessor:
|
|
347
346
|
|
348
347
|
### CODE TO ENSURE THAT STUDENT NOTEBOOK IS IMPORTABLE
|
349
348
|
if "question_path" in locals():
|
350
|
-
|
351
349
|
# question_root_path = os.path.dirname(question_path)
|
352
350
|
question_file_name = os.path.basename(question_path)
|
353
|
-
question_file_name_sanitized = sanitize_string(
|
351
|
+
question_file_name_sanitized = sanitize_string(
|
352
|
+
question_file_name.replace("_questions", "")
|
353
|
+
)
|
354
354
|
if question_file_name_sanitized.endswith("_py"):
|
355
355
|
question_file_name_sanitized = question_file_name_sanitized[:-3] + ".py"
|
356
|
-
|
356
|
+
|
357
357
|
# Rename the file
|
358
|
-
os.rename(
|
359
|
-
|
358
|
+
os.rename(
|
359
|
+
os.path.join(
|
360
|
+
student_path, question_file_name.replace("_questions", "")
|
361
|
+
),
|
362
|
+
os.path.join(student_path, question_file_name_sanitized),
|
363
|
+
)
|
364
|
+
|
360
365
|
# Ensure the "questions" folder exists
|
361
366
|
questions_folder_jbook = os.path.join(self.root_folder, "questions")
|
362
367
|
os.makedirs(questions_folder_jbook, exist_ok=True)
|
363
|
-
|
364
|
-
# Copy the renamed file to the "questions" folder
|
365
|
-
shutil.copy(os.path.join(student_path, question_file_name_sanitized), os.path.join(questions_folder_jbook, question_file_name_sanitized))
|
366
368
|
|
369
|
+
# Copy the renamed file to the "questions" folder
|
370
|
+
shutil.copy(
|
371
|
+
os.path.join(student_path, question_file_name_sanitized),
|
372
|
+
os.path.join(questions_folder_jbook, question_file_name_sanitized),
|
373
|
+
)
|
367
374
|
|
368
375
|
@staticmethod
|
369
376
|
def replace_temp_in_notebook(input_file, output_file):
|
@@ -434,7 +441,7 @@ class NotebookProcessor:
|
|
434
441
|
# {"Q2": {"question_text": "What is 3+3?", "points": 3.0}}
|
435
442
|
# ]
|
436
443
|
"""
|
437
|
-
merged_data = []
|
444
|
+
# merged_data = []
|
438
445
|
|
439
446
|
# Loop through each question set in the data
|
440
447
|
for i, _data in enumerate(data):
|
@@ -454,7 +461,7 @@ class NotebookProcessor:
|
|
454
461
|
grade_ = [raw[i]["grade"]]
|
455
462
|
|
456
463
|
# Merge each question's metadata with corresponding raw metadata
|
457
|
-
for j, (key,
|
464
|
+
for j, (key, _) in enumerate(_data.items()):
|
458
465
|
# Combine raw metadata with question data
|
459
466
|
data[i][key] = data[i][key] | raw[i]
|
460
467
|
# Assign the correct point value to the question
|
@@ -576,52 +583,6 @@ class NotebookProcessor:
|
|
576
583
|
f.write(f' "{key}": {repr(solution)},\n')
|
577
584
|
f.write("}\n")
|
578
585
|
|
579
|
-
@staticmethod
|
580
|
-
def generate_solution_MCQ(data_list, output_file="output.py"):
|
581
|
-
"""
|
582
|
-
Generates a Python file with solutions and total points based on the input data.
|
583
|
-
If the file already exists, it appends new solutions to the existing solution dictionary.
|
584
|
-
|
585
|
-
Args:
|
586
|
-
data_list (list): A list of dictionaries containing question metadata.
|
587
|
-
output_file (str): Path to the output Python file.
|
588
|
-
"""
|
589
|
-
|
590
|
-
solutions = {}
|
591
|
-
total_points = 0.0
|
592
|
-
|
593
|
-
# If the output file exists, load the existing solutions and total_points
|
594
|
-
if os.path.exists(output_file):
|
595
|
-
spec = importlib.util.spec_from_file_location(
|
596
|
-
"existing_module", output_file
|
597
|
-
)
|
598
|
-
existing_module = importlib.util.module_from_spec(spec)
|
599
|
-
spec.loader.exec_module(existing_module) # Load the module dynamically
|
600
|
-
|
601
|
-
# Attempt to read existing solutions and total_points
|
602
|
-
if hasattr(existing_module, "solutions"):
|
603
|
-
solutions.update(existing_module.solutions)
|
604
|
-
if hasattr(existing_module, "total_points"):
|
605
|
-
total_points += existing_module.total_points
|
606
|
-
|
607
|
-
# Process new question data and update solutions and total_points
|
608
|
-
for question_set in data_list:
|
609
|
-
for key, question_data in question_set.items():
|
610
|
-
solution_key = f"q{question_data['question number']}-{question_data['subquestion_number']}-{key}"
|
611
|
-
solutions[solution_key] = question_data["solution"]
|
612
|
-
total_points += question_data["points"]
|
613
|
-
|
614
|
-
# Write updated total_points and solutions back to the file
|
615
|
-
with open(output_file, "w", encoding="utf-8") as f:
|
616
|
-
f.write("from typing import Any\n\n")
|
617
|
-
f.write(f"total_points: float = {total_points}\n\n")
|
618
|
-
|
619
|
-
f.write("solutions: dict[str, Any] = {\n")
|
620
|
-
for key, solution in solutions.items():
|
621
|
-
# For safety, we assume solutions are strings, but if not, repr would be safer
|
622
|
-
f.write(f' "{key}": {repr(solution)},\n')
|
623
|
-
f.write("}\n")
|
624
|
-
|
625
586
|
def extract_MCQ(ipynb_file):
|
626
587
|
"""
|
627
588
|
Extracts questions from markdown cells and organizes them as a nested dictionary,
|
@@ -1314,7 +1275,7 @@ def generate_mcq_file(data_dict, output_file="mc_questions.py"):
|
|
1314
1275
|
]
|
1315
1276
|
|
1316
1277
|
# Ensure header lines are present
|
1317
|
-
|
1278
|
+
_existing_content = ensure_imports(output_file, header_lines)
|
1318
1279
|
|
1319
1280
|
for question_dict in data_dict:
|
1320
1281
|
with open(output_file, "a", encoding="utf-8") as f:
|
@@ -1383,7 +1344,7 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
|
|
1383
1344
|
]
|
1384
1345
|
|
1385
1346
|
# Ensure header lines are present
|
1386
|
-
|
1347
|
+
_existing_content = ensure_imports(output_file, header_lines)
|
1387
1348
|
|
1388
1349
|
for question_dict in data_dict:
|
1389
1350
|
with open(output_file, "a", encoding="utf-8") as f:
|
@@ -1458,7 +1419,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
|
|
1458
1419
|
]
|
1459
1420
|
|
1460
1421
|
# Ensure header lines are present
|
1461
|
-
|
1422
|
+
_existing_content = ensure_imports(output_file, header_lines)
|
1462
1423
|
|
1463
1424
|
for question_dict in data_dict:
|
1464
1425
|
with open(output_file, "a", encoding="utf-8") as f:
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Auto-generated __init__.py
|
2
|
+
|
3
|
+
from . import (
|
4
|
+
multiple_choice,
|
5
|
+
reading_question,
|
6
|
+
select_many,
|
7
|
+
student_info,
|
8
|
+
true_false,
|
9
|
+
types_question,
|
10
|
+
)
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"select_many",
|
14
|
+
"multiple_choice",
|
15
|
+
"true_false",
|
16
|
+
"reading_question",
|
17
|
+
"student_info",
|
18
|
+
"types_question",
|
19
|
+
]
|
@@ -1,13 +1,10 @@
|
|
1
|
-
from typing import Tuple
|
1
|
+
from typing import List, Tuple
|
2
2
|
|
3
3
|
import panel as pn
|
4
4
|
|
5
|
-
from ..utils import list_of_lists
|
6
5
|
from ..widgets_base.select import SelectQuestion
|
7
6
|
from .style import drexel_colors, raw_css
|
8
7
|
|
9
|
-
import panel as pn
|
10
|
-
|
11
8
|
# Pass the custom CSS to Panel
|
12
9
|
pn.extension(design="material", global_css=[drexel_colors], raw_css=[raw_css])
|
13
10
|
|
@@ -16,10 +13,6 @@ pn.extension(design="material", global_css=[drexel_colors], raw_css=[raw_css])
|
|
16
13
|
#
|
17
14
|
|
18
15
|
|
19
|
-
import panel as pn
|
20
|
-
from typing import List, Tuple
|
21
|
-
|
22
|
-
|
23
16
|
def TrueFalse_style(
|
24
17
|
descriptions: List[str],
|
25
18
|
options: List[str] | List[List[str]],
|
@@ -37,7 +30,7 @@ def TrueFalse_style(
|
|
37
30
|
Tuple[List[pn.pane.HTML], List[pn.widgets.RadioBoxGroup]]: Styled description panes and radio button groups.
|
38
31
|
"""
|
39
32
|
desc_width = "100%" # Responsive width for descriptions
|
40
|
-
button_width = "100%" # Responsive width for radio buttons
|
33
|
+
# button_width = "100%" # Responsive width for radio buttons
|
41
34
|
|
42
35
|
# Create description widgets
|
43
36
|
desc_widgets = [
|
@@ -73,8 +66,8 @@ def TrueFalse_style(
|
|
73
66
|
# Question class
|
74
67
|
#
|
75
68
|
|
76
|
-
class TFQuestion(SelectQuestion):
|
77
69
|
|
70
|
+
class TFQuestion(SelectQuestion):
|
78
71
|
def __init__(
|
79
72
|
self,
|
80
73
|
title="Select if the statement is True or False",
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import time
|
1
2
|
from typing import Callable, Tuple
|
2
3
|
|
3
4
|
import panel as pn
|
@@ -5,8 +6,6 @@ import panel as pn
|
|
5
6
|
from ..telemetry import ensure_responses, update_responses
|
6
7
|
from ..utils import shuffle_questions
|
7
8
|
from ..widgets.style import drexel_colors
|
8
|
-
import time
|
9
|
-
from IPython.display import update_display, display
|
10
9
|
|
11
10
|
# Pass the custom CSS to Panel
|
12
11
|
pn.extension(design="material", global_css=[drexel_colors])
|
@@ -75,7 +74,7 @@ class SelectQuestion:
|
|
75
74
|
self.submit_button.name = "Responses Submitted"
|
76
75
|
time.sleep(1)
|
77
76
|
self.submit_button.name = "Submit"
|
78
|
-
|
77
|
+
|
79
78
|
# # Display the message with a unique display_id
|
80
79
|
# display_id = "temp_message"
|
81
80
|
# display("Responses recorded successfully", display_id=display_id)
|
@@ -1 +0,0 @@
|
|
1
|
-
{"seed": 30, "MC1": "True", "MC2": "True", "MC3": "False", "MC4": "False"}
|
@@ -1,10 +0,0 @@
|
|
1
|
-
# Auto-generated __init__.py
|
2
|
-
|
3
|
-
from . import select_many
|
4
|
-
from . import multiple_choice
|
5
|
-
from . import true_false
|
6
|
-
from . import reading_question
|
7
|
-
from . import student_info
|
8
|
-
from . import types_question
|
9
|
-
|
10
|
-
__all__ = ['select_many', 'multiple_choice', 'true_false', 'reading_question', 'student_info', 'types_question']
|
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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import panel as pn
|
2
2
|
|
3
|
-
from ..widgets_base.multi_select import MultiSelectQuestion
|
4
3
|
from ..widgets.style import drexel_colors, raw_css
|
4
|
+
from ..widgets_base.multi_select import MultiSelectQuestion
|
5
5
|
|
6
6
|
# Pass the custom CSS to Panel
|
7
7
|
pn.extension(design="material", global_css=[drexel_colors], raw_css=[raw_css])
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|