PyKubeGrader 0.2.26__tar.gz → 0.2.27__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {pykubegrader-0.2.26/src/PyKubeGrader.egg-info → pykubegrader-0.2.27}/PKG-INFO +2 -2
- {pykubegrader-0.2.26 → pykubegrader-0.2.27/src/PyKubeGrader.egg-info}/PKG-INFO +2 -2
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/build/api_notebook_builder.py +6 -5
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/build/build_folder.py +6 -12
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/telemetry.py +2 -2
- pykubegrader-0.2.27/src/pykubegrader/tokens/tokens.py +42 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/tokens/validate_token.py +16 -14
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets_base/reading.py +4 -4
- pykubegrader-0.2.26/src/pykubegrader/tokens/tokens.py +0 -47
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/.coveragerc +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/.github/workflows/main.yml +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/.gitignore +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/.readthedocs.yml +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/AUTHORS.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/CHANGELOG.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/CONTRIBUTING.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/LICENSE.txt +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/README.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/Makefile +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/_static/Drexel_blue_Logo_square_Dark.png +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/_static/Drexel_blue_Logo_square_Light.png +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/_static/custom.css +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/authors.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/changelog.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/conf.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/contributing.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/index.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/license.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/readme.rst +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/docs/requirements.txt +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/examples/.responses.json +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/examples/true_false.ipynb +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/pyproject.toml +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/setup.cfg +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/setup.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/PyKubeGrader.egg-info/SOURCES.txt +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/PyKubeGrader.egg-info/dependency_links.txt +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/PyKubeGrader.egg-info/entry_points.txt +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/PyKubeGrader.egg-info/not-zip-safe +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/PyKubeGrader.egg-info/requires.txt +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/PyKubeGrader.egg-info/top_level.txt +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/__init__.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/build/__init__.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/build/clean_folder.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/graders/__init__.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/graders/late_assignments.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/initialize.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/log_parser/__init__.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/log_parser/parse.ipynb +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/log_parser/parse.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/submit/submit_assignment.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/utils.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/validate.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/__init__.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/multiple_choice.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/reading_question.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/select_many.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/student_info.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/style.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/true_false.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets/types_question.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets_base/__init__.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets_base/multi_select.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/src/pykubegrader/widgets_base/select.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/tests/conftest.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/tests/import_test.py +0 -0
- {pykubegrader-0.2.26 → pykubegrader-0.2.27}/tox.ini +0 -0
@@ -65,9 +65,6 @@ class FastAPINotebookBuilder:
|
|
65
65
|
updated_cell_source.extend(
|
66
66
|
FastAPINotebookBuilder.construct_question_info(cell_dict)
|
67
67
|
)
|
68
|
-
updated_cell_source.extend(
|
69
|
-
FastAPINotebookBuilder.construct_update_responses(cell_dict)
|
70
|
-
)
|
71
68
|
|
72
69
|
updated_cell_source.extend(cell_source[last_import_line_ind + 1 :])
|
73
70
|
updated_cell_source.extend(["\n"])
|
@@ -91,6 +88,10 @@ class FastAPINotebookBuilder:
|
|
91
88
|
["os.environ['EARNED_POINTS'] = str(earned_points)\n"]
|
92
89
|
)
|
93
90
|
|
91
|
+
updated_cell_source.extend(
|
92
|
+
FastAPINotebookBuilder.construct_update_responses(cell_dict)
|
93
|
+
)
|
94
|
+
|
94
95
|
self.replace_cell_source(cell_index, updated_cell_source)
|
95
96
|
|
96
97
|
def compute_max_points_free_response(self):
|
@@ -142,7 +143,7 @@ class FastAPINotebookBuilder:
|
|
142
143
|
|
143
144
|
for logging_variable in logging_variables:
|
144
145
|
update_responses.append(
|
145
|
-
f"responses = update_responses(question_id, {logging_variable})\n"
|
146
|
+
f"responses = update_responses(question_id, str({logging_variable}))\n"
|
146
147
|
)
|
147
148
|
|
148
149
|
return update_responses
|
@@ -189,7 +190,7 @@ class FastAPINotebookBuilder:
|
|
189
190
|
question_id = cell_dict["question"] + "-" + str(cell_dict["test_number"])
|
190
191
|
|
191
192
|
question_info.append(f'question_id = "{question_id}"' + "\n")
|
192
|
-
question_info.append(f
|
193
|
+
question_info.append(f"max_score = {cell_dict['points']}\n")
|
193
194
|
question_info.append("score = 0\n")
|
194
195
|
|
195
196
|
return question_info
|
@@ -791,7 +791,7 @@ class NotebookProcessor:
|
|
791
791
|
data, output_file=solution_path
|
792
792
|
)
|
793
793
|
|
794
|
-
question_path = f"{new_notebook_path.replace(
|
794
|
+
question_path = f"{new_notebook_path.replace('.ipynb', '')}_questions.py"
|
795
795
|
|
796
796
|
generate_mcq_file(data, output_file=question_path)
|
797
797
|
|
@@ -831,7 +831,7 @@ class NotebookProcessor:
|
|
831
831
|
data, output_file=solution_path
|
832
832
|
)
|
833
833
|
|
834
|
-
question_path = f"{new_notebook_path.replace(
|
834
|
+
question_path = f"{new_notebook_path.replace('.ipynb', '')}_questions.py"
|
835
835
|
|
836
836
|
generate_tf_file(data, output_file=question_path)
|
837
837
|
|
@@ -869,7 +869,7 @@ class NotebookProcessor:
|
|
869
869
|
data, output_file=solution_path
|
870
870
|
)
|
871
871
|
|
872
|
-
question_path = f"{new_notebook_path.replace(
|
872
|
+
question_path = f"{new_notebook_path.replace('.ipynb', '')}_questions.py"
|
873
873
|
|
874
874
|
generate_select_many_file(data, output_file=question_path)
|
875
875
|
|
@@ -1809,9 +1809,7 @@ def generate_mcq_file(data_dict, output_file="mc_questions.py"):
|
|
1809
1809
|
)
|
1810
1810
|
f.write(" def __init__(self):\n")
|
1811
1811
|
f.write(" super().__init__(\n")
|
1812
|
-
f.write(
|
1813
|
-
f' title=f"Question{q_value['question number']}: {q_value['title']}",\n'
|
1814
|
-
)
|
1812
|
+
f.write(f' title=f"{q_value["title"]}",\n')
|
1815
1813
|
f.write(" style=MCQ,\n")
|
1816
1814
|
f.write(
|
1817
1815
|
f" question_number={q_value['question number']},\n"
|
@@ -1882,9 +1880,7 @@ def generate_select_many_file(data_dict, output_file="select_many_questions.py")
|
|
1882
1880
|
)
|
1883
1881
|
f.write(" def __init__(self):\n")
|
1884
1882
|
f.write(" super().__init__(\n")
|
1885
|
-
f.write(
|
1886
|
-
f' title=f"Question{q_value['question number']}: {q_value['title']}",\n'
|
1887
|
-
)
|
1883
|
+
f.write(f' title=f"{q_value["title"]}",\n')
|
1888
1884
|
f.write(" style=MultiSelect,\n")
|
1889
1885
|
f.write(
|
1890
1886
|
f" question_number={q_value['question number']},\n"
|
@@ -1961,9 +1957,7 @@ def generate_tf_file(data_dict, output_file="tf_questions.py"):
|
|
1961
1957
|
)
|
1962
1958
|
f.write(" def __init__(self):\n")
|
1963
1959
|
f.write(" super().__init__(\n")
|
1964
|
-
f.write(
|
1965
|
-
f' title=f"Question{q_value['question number']}: {q_value['title']}",\n'
|
1966
|
-
)
|
1960
|
+
f.write(f' title=f"{q_value["title"]}",\n')
|
1967
1961
|
f.write(" style=TFStyle,\n")
|
1968
1962
|
f.write(
|
1969
1963
|
f" question_number={q_value['question number']},\n"
|
@@ -134,11 +134,11 @@ def score_question(
|
|
134
134
|
responses = ensure_responses()
|
135
135
|
|
136
136
|
payload: dict[str, Any] = {
|
137
|
-
"student_email": f
|
137
|
+
"student_email": f"{responses['jhub_user']}@drexel.edu",
|
138
138
|
"term": term,
|
139
139
|
"week": responses["week"],
|
140
140
|
"assignment": responses["assignment_type"],
|
141
|
-
"question": f
|
141
|
+
"question": f"_{responses['assignment']}",
|
142
142
|
"responses": responses,
|
143
143
|
}
|
144
144
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from requests.auth import HTTPBasicAuth
|
5
|
+
|
6
|
+
|
7
|
+
def build_token_payload(token: str, duration: int) -> dict:
|
8
|
+
jhub_user = os.getenv("JUPYTERHUB_USER")
|
9
|
+
if jhub_user is None:
|
10
|
+
raise ValueError("JupyterHub user not found")
|
11
|
+
|
12
|
+
return {
|
13
|
+
"value": token,
|
14
|
+
"duration": duration,
|
15
|
+
"requester": jhub_user,
|
16
|
+
}
|
17
|
+
|
18
|
+
|
19
|
+
def add_token(token: str, duration: int = 20) -> None:
|
20
|
+
"""
|
21
|
+
Sends a POST request to mint a token
|
22
|
+
"""
|
23
|
+
|
24
|
+
url = "https://engr-131-api.eastus.cloudapp.azure.com/tokens"
|
25
|
+
|
26
|
+
payload = build_token_payload(token=token, duration=duration)
|
27
|
+
|
28
|
+
# Dummy credentials for HTTP Basic Auth
|
29
|
+
auth = HTTPBasicAuth("user", "password")
|
30
|
+
|
31
|
+
# Add a custom header, for potential use in authorization
|
32
|
+
headers = {"x-jhub-user": payload["requester"]}
|
33
|
+
|
34
|
+
response = requests.post(url=url, json=payload, headers=headers, auth=auth)
|
35
|
+
|
36
|
+
# Print response
|
37
|
+
print(f"Status code: {response.status_code}")
|
38
|
+
|
39
|
+
try:
|
40
|
+
print(f"Response: {response.json()}")
|
41
|
+
except ValueError:
|
42
|
+
print(f"Response: {response.text}")
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import asyncio
|
2
|
-
import base64
|
3
2
|
import os
|
4
3
|
from typing import Optional
|
5
4
|
|
@@ -15,29 +14,33 @@ class TokenValidationError(Exception):
|
|
15
14
|
Custom exception raised when the token validation fails.
|
16
15
|
"""
|
17
16
|
|
18
|
-
def __init__(self, message=None):
|
17
|
+
def __init__(self, message: Optional[str] = None):
|
19
18
|
"""
|
20
19
|
Initialize the exception with an optional message.
|
21
20
|
|
22
21
|
Args:
|
23
22
|
message (str, optional): The error message to display. Defaults to None.
|
24
23
|
"""
|
24
|
+
|
25
25
|
super().__init__(message)
|
26
26
|
|
27
27
|
|
28
|
-
def get_credentials():
|
28
|
+
def get_credentials() -> dict[str, str]:
|
29
29
|
"""
|
30
30
|
Fetch the username and password from environment variables.
|
31
31
|
|
32
32
|
Returns:
|
33
33
|
dict: A dictionary containing 'username' and 'password'.
|
34
34
|
"""
|
35
|
+
|
35
36
|
username = os.getenv("user_name_student")
|
36
37
|
password = os.getenv("keys_student")
|
38
|
+
|
37
39
|
if not username or not password:
|
38
40
|
raise ValueError(
|
39
|
-
"Environment
|
41
|
+
"Environment variable 'user_name_student' or 'keys_student' not set"
|
40
42
|
)
|
43
|
+
|
41
44
|
return {"username": username, "password": password}
|
42
45
|
|
43
46
|
|
@@ -55,19 +58,22 @@ async def async_validate_token(token: Optional[str] = None) -> None:
|
|
55
58
|
None: If the token is valid, the function will pass silently.
|
56
59
|
"""
|
57
60
|
|
61
|
+
# First, check if token is provided as an argument
|
58
62
|
if token is not None:
|
59
63
|
os.environ["TOKEN"] = token
|
60
64
|
|
65
|
+
# Next, check if token is available in environment variables
|
61
66
|
if token is None:
|
62
67
|
token = os.getenv("TOKEN", None)
|
63
68
|
|
69
|
+
# Otherwise, raise an error
|
64
70
|
if token is None:
|
65
71
|
raise TokenValidationError("No token provided")
|
66
72
|
|
67
73
|
# Fetch the endpoint URL
|
68
74
|
base_url = os.getenv("DB_URL")
|
69
75
|
if not base_url:
|
70
|
-
raise ValueError("Environment variable 'DB_URL'
|
76
|
+
raise ValueError("Environment variable 'DB_URL' not set")
|
71
77
|
endpoint = f"{base_url}validate-token/{token}"
|
72
78
|
|
73
79
|
# Get credentials
|
@@ -75,17 +81,12 @@ async def async_validate_token(token: Optional[str] = None) -> None:
|
|
75
81
|
username = credentials["username"]
|
76
82
|
password = credentials["password"]
|
77
83
|
|
78
|
-
|
79
|
-
auth_header = (
|
80
|
-
f"Basic {base64.b64encode(f'{username}:{password}'.encode()).decode()}"
|
81
|
-
)
|
84
|
+
basic_auth = httpx.BasicAuth(username=username, password=password)
|
82
85
|
|
83
|
-
# Make
|
86
|
+
# Make GET request
|
84
87
|
async with httpx.AsyncClient() as client:
|
85
88
|
try:
|
86
|
-
response = await client.get(
|
87
|
-
endpoint, headers={"Authorization": auth_header}, timeout=10
|
88
|
-
)
|
89
|
+
response = await client.get(url=endpoint, auth=basic_auth, timeout=10)
|
89
90
|
|
90
91
|
if response.status_code == 200:
|
91
92
|
# If the response is 200, the token is valid
|
@@ -133,8 +134,9 @@ def validate_token(token: Optional[str] = None) -> None:
|
|
133
134
|
# Example usage
|
134
135
|
if __name__ == "__main__":
|
135
136
|
token = "test"
|
137
|
+
|
136
138
|
try:
|
137
139
|
validate_token(token)
|
138
|
-
print("Token is valid
|
140
|
+
print("Token is valid")
|
139
141
|
except TokenValidationError as e:
|
140
142
|
print(f"Token validation failed: {e}")
|
@@ -23,7 +23,7 @@ class ReadingPython:
|
|
23
23
|
|
24
24
|
# Dynamically assign attributes based on keys, with default values from responses
|
25
25
|
for num in range(len(options["lines_to_comment"]) + options["n_rows"]):
|
26
|
-
key = f"q{question_number}_{num+1}"
|
26
|
+
key = f"q{question_number}_{num + 1}"
|
27
27
|
|
28
28
|
# Dynamically assign the value from the responses file for persistence
|
29
29
|
if num < len(options["lines_to_comment"]):
|
@@ -57,7 +57,7 @@ class ReadingPython:
|
|
57
57
|
line: pn.widgets.Select(
|
58
58
|
options=shuffle_options(options["comments_options"], seed),
|
59
59
|
name=f"Line {line}:",
|
60
|
-
value=getattr(self, f"q{question_number}_{i_comments+1}"),
|
60
|
+
value=getattr(self, f"q{question_number}_{i_comments + 1}"),
|
61
61
|
width=600,
|
62
62
|
)
|
63
63
|
for i_comments, line in enumerate(options["lines_to_comment"])
|
@@ -97,13 +97,13 @@ class ReadingPython:
|
|
97
97
|
# Function to create a row with dropdowns
|
98
98
|
def create_row(step: int) -> pn.Row:
|
99
99
|
widgets_list = [
|
100
|
-
pn.pane.HTML(f"Step {step+1}", width=150)
|
100
|
+
pn.pane.HTML(f"Step {step + 1}", width=150)
|
101
101
|
if i == 0
|
102
102
|
else pn.widgets.Select(
|
103
103
|
options=dropdown_options[i - 1],
|
104
104
|
value=getattr(
|
105
105
|
self,
|
106
|
-
f
|
106
|
+
f"q{question_number}_{len(options['lines_to_comment']) + step + 1}",
|
107
107
|
)[i - 1],
|
108
108
|
width=150,
|
109
109
|
)
|
@@ -1,47 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import os
|
3
|
-
|
4
|
-
import requests
|
5
|
-
|
6
|
-
|
7
|
-
def build_token_payload(token: str, duration: int) -> dict:
|
8
|
-
if os.getenv("JUPYTERHUB_USER", None) is None:
|
9
|
-
raise ValueError("JupyterHub user not found")
|
10
|
-
|
11
|
-
# Return the extracted details as a dictionary
|
12
|
-
return {
|
13
|
-
"value": token,
|
14
|
-
"requester": os.getenv("JUPYTERHUB_USER", None),
|
15
|
-
"duration": duration,
|
16
|
-
}
|
17
|
-
|
18
|
-
|
19
|
-
# Need to do for add token as well
|
20
|
-
def add_token(token, duration=20):
|
21
|
-
"""
|
22
|
-
Sends a POST request to add a notebook.
|
23
|
-
"""
|
24
|
-
# Define the URL
|
25
|
-
url = "https://engr-131-api.eastus.cloudapp.azure.com/tokens"
|
26
|
-
|
27
|
-
# Build the payload
|
28
|
-
payload = build_token_payload(token=token, duration=duration)
|
29
|
-
|
30
|
-
# Define HTTP Basic Authentication
|
31
|
-
auth = ("user", "password")
|
32
|
-
|
33
|
-
# Define headers
|
34
|
-
headers = {"Content-Type": "application/json"}
|
35
|
-
|
36
|
-
# Serialize the payload with the custom JSON encoder
|
37
|
-
serialized_payload = json.dumps(payload)
|
38
|
-
|
39
|
-
# Send the POST request
|
40
|
-
response = requests.post(url, data=serialized_payload, headers=headers, auth=auth)
|
41
|
-
|
42
|
-
# Print the response
|
43
|
-
print(f"Status Code: {response.status_code}")
|
44
|
-
try:
|
45
|
-
print(f"Response: {response.json()}")
|
46
|
-
except ValueError:
|
47
|
-
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
|