Skip to content

Commit 476d97f

Browse files
committed
Improve static typing and mypy compatibility
- Update type annotations across multiple scripts - Add explicit type casts where needed - Refactor functions to pass mypy strict mode - Add mypy.ini basic configuration for Python 3.11
1 parent 671147d commit 476d97f

10 files changed

Lines changed: 124 additions & 67 deletions

mypi.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[mypy]
2+
python_version = 3.11
3+
strict = True

stuff/lint.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
set -e
3+
flake8 utils/
4+
mypy utils/

utils/Pipfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ name = "pypi"
99

1010
mypy = ">=0.782"
1111
pycodestyle = ">=2.6.0"
12-
12+
flake8 = ">=3.9.2"
1313

1414
[packages]
1515

utils/container.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os.path
66

77
from types import TracebackType
8-
from typing import AnyStr, Iterator, IO, Optional, Type, Sequence
8+
from typing import Iterator, IO, Optional, Type, Sequence
99

1010
import problems
1111

@@ -16,7 +16,7 @@
1616

1717
@contextlib.contextmanager
1818
def _maybe_open(path: Optional[str],
19-
mode: str) -> Iterator[Optional[IO[AnyStr]]]:
19+
mode: str) -> Iterator[Optional[IO[str]]]:
2020
"""A contextmanager that can open a file, or return None.
2121
2222
This is useful to provide arguments to subprocess.call() and its friends.

utils/download_and_sync_courses.py

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
import os
77
import zipfile
8-
from typing import Dict, Any
8+
from typing import Dict, Any, List, NamedTuple, cast
99
from urllib.parse import urlparse, urljoin
1010
import omegaup.api
1111
import shutil
@@ -18,8 +18,6 @@
1818
logging.basicConfig(level=logging.INFO)
1919
LOG = logging.getLogger(__name__)
2020

21-
API_CLIENT = None
22-
BASE_URL = None
2321

2422
# 👇 Add your course aliases here
2523
COURSE_ALIASES = [
@@ -31,11 +29,15 @@
3129
os.path.join(os.path.dirname(__file__), "..", "Courses"))
3230

3331

34-
def handle_input():
35-
global BASE_URL, API_TOKEN
32+
class InputArgs(NamedTuple):
33+
api_token: str
34+
base_url: str
35+
36+
37+
def handle_input() -> InputArgs:
3638
parser = argparse.ArgumentParser(
37-
description=f"Download and extract problems from multiple course"
38-
f" assignments"
39+
description="Download and extract problems from multiple course"
40+
" assignments"
3941
)
4042
parser.add_argument("--url",
4143
default="https://omegaup.com",
@@ -45,23 +47,33 @@ def handle_input():
4547
default=os.environ.get("OMEGAUP_API_TOKEN"),
4648
required=("OMEGAUP_API_TOKEN" not in os.environ))
4749
args = parser.parse_args()
48-
BASE_URL = args.url
49-
return args.api_token
50+
if args.api_token is None:
51+
parser.error(
52+
"API token is required (use --api-token or set OMEGAUP_API_TOKEN)")
53+
return InputArgs(api_token=str(args.api_token), base_url=str(args.url))
5054

5155

52-
def get_json(endpoint: str, params: Dict[str, str]) -> Dict[str, Any]:
53-
return API_CLIENT.query(endpoint, params)
56+
def get_json(
57+
client: omegaup.api.Client,
58+
endpoint: str,
59+
params: Dict[str, str],
60+
base_url: str
61+
) -> Dict[str, Any]:
62+
return cast(Dict[str, Any], client.query(endpoint, params))
5463

5564

5665
def sanitize_filename(name: str) -> str:
5766
return "".join(c for c in name if c.isalnum() or c in " -_").strip()
5867

5968

6069
def get_course_details(
70+
client: omegaup.api.Client,
6171
course_alias: str,
62-
course_base_folder: str
72+
course_base_folder: str,
73+
base_url: str
6374
) -> Dict[str, Any]:
64-
details = get_json("/api/course/details/", {"alias": course_alias})
75+
params = {"alias": course_alias}
76+
details = get_json(client, "/api/course/details/", params, base_url)
6577
details.pop("assignments", None)
6678
details.pop("clarifications", None)
6779

@@ -76,29 +88,49 @@ def get_course_details(
7688
return details
7789

7890

79-
def get_assignments(course_alias: str):
80-
return get_json("/api/course/listAssignments/",
81-
{"course_alias": course_alias})["assignments"]
91+
def get_assignments(
92+
client: omegaup.api.Client,
93+
course_alias: str,
94+
base_url: str
95+
) -> List[Dict[str, Any]]:
96+
endpoint = "/api/course/listAssignments/"
97+
params = {"course_alias": course_alias}
98+
return cast(
99+
List[Dict[str, Any]],
100+
get_json(client, endpoint, params, base_url)["assignments"]
101+
)
82102

83103

84-
def get_assignment_details(course_alias: str, assignment_alias: str):
85-
return get_json("/api/course/assignmentDetails/", {
104+
def get_assignment_details(
105+
client: omegaup.api.Client,
106+
course_alias: str,
107+
assignment_alias: str,
108+
base_url: str
109+
) -> Dict[str, Any]:
110+
endpoint = "/api/course/assignmentDetails/"
111+
params = {
86112
"course": course_alias,
87113
"assignment": assignment_alias
88-
})
114+
}
115+
return get_json(client, endpoint, params, base_url)
89116

90117

91-
def download_and_unzip(problem_alias: str, assignment_folder: str) -> bool:
118+
def download_and_unzip(client: omegaup.api.Client, problem_alias: str,
119+
assignment_folder: str, base_url: str) -> bool:
92120
try:
93121
download_url = urljoin(
94-
BASE_URL,
122+
base_url,
95123
f"/api/problem/download/problem_alias/{problem_alias}/"
96124
)
97125
parsed_url = urlparse(download_url)
126+
if parsed_url.hostname is None:
127+
LOG.error(f"Invalid download URL (missing hostname): "
128+
f"{download_url}")
129+
return False
98130
conn = http.client.HTTPSConnection(parsed_url.hostname,
99131
context=context)
100132

101-
headers = {'Authorization': f'token {API_CLIENT.api_token}'}
133+
headers = {'Authorization': f'token {client.api_token}'}
102134
path = parsed_url.path
103135

104136
conn.request("GET", path, headers=headers)
@@ -171,10 +203,9 @@ def download_and_unzip(problem_alias: str, assignment_folder: str) -> bool:
171203
return False
172204

173205

174-
def main():
175-
global API_CLIENT
176-
api_token = handle_input()
177-
API_CLIENT = omegaup.api.Client(api_token=api_token, url=BASE_URL)
206+
def main() -> None:
207+
input = handle_input()
208+
client = omegaup.api.Client(api_token=input.api_token, url=input.base_url)
178209

179210
if os.path.exists(BASE_COURSE_FOLDER):
180211
LOG.warning("Delete existing course folder to avoid conflicts")
@@ -186,7 +217,7 @@ def main():
186217
for course_alias in COURSE_ALIASES:
187218
LOG.info(f"📘 Starting course: {course_alias}")
188219
try:
189-
assignments = get_assignments(course_alias)
220+
assignments = get_assignments(client, course_alias, input.base_url)
190221

191222
if not assignments:
192223
LOG.warning(f"No assignments found in {course_alias}.")
@@ -203,8 +234,12 @@ def main():
203234
)
204235

205236
try:
206-
details = get_assignment_details(course_alias,
207-
assignment_alias)
237+
details = get_assignment_details(
238+
client,
239+
course_alias,
240+
assignment_alias,
241+
input.base_url
242+
)
208243
assignment_folder = os.path.join(course_folder,
209244
assignment_alias)
210245
os.makedirs(assignment_folder, exist_ok=True)
@@ -213,8 +248,10 @@ def main():
213248

214249
for problem in problems:
215250
try:
216-
downloaded = download_and_unzip(problem["alias"],
217-
assignment_folder)
251+
downloaded = download_and_unzip(client,
252+
problem["alias"],
253+
assignment_folder,
254+
input.base_url)
218255
if downloaded:
219256
rel_path = os.path.join(
220257
"Courses",

utils/generateresources.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import argparse
33
import concurrent.futures
44
import datetime
5-
import json
65
import logging
76
import os
87
import re

utils/runtests.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import argparse
33
import collections
44
import concurrent.futures
5-
import decimal
65
import json
76
import logging
87
import os
@@ -289,9 +288,6 @@ def _main() -> None:
289288
failureMessages: DefaultDict[
290289
str, List[str]] = collections.defaultdict(list)
291290

292-
normalizedScore = decimal.Decimal(got.get('score', 0))
293-
scaledScore = round(normalizedScore, 15) * 100
294-
295291
if testResult['state'] != 'passed':
296292
# Build a table that reports groups and case verdicts.
297293
groupReportTable = [

utils/update_assignment_problems.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import logging
66
import os
77
import datetime
8-
from typing import Dict, Any, List, NamedTuple, Tuple
8+
from typing import Dict, Any, List, NamedTuple, Sequence, Tuple
99
import omegaup.api
10+
from omegaup.api import _CourseAssignment
1011
import re
1112
from urllib.parse import urlparse, urljoin
1213
import shutil
@@ -67,7 +68,10 @@ def handle_input() -> Tuple[str, str, str]:
6768
return args.api_token, args.url, args.input
6869

6970

70-
def assignment_exists(assignments: List[Any], alias: str) -> bool:
71+
def assignment_exists(
72+
assignments: Sequence[_CourseAssignment],
73+
alias: str
74+
) -> bool:
7175
return any(a.alias == alias for a in assignments)
7276

7377

@@ -91,8 +95,8 @@ def create_assignment(
9195
assignment_type="homework",
9296
name=assignment_alias,
9397
description=f"Auto-created assignment {assignment_alias}",
94-
start_time=int(now.timestamp()),
95-
finish_time=int(finish.timestamp()),
98+
start_time=now,
99+
finish_time=finish,
96100
unlimited_duration=True
97101
)
98102
LOG.info(f"✅ Created assignment '{assignment_alias}'")
@@ -112,6 +116,10 @@ def download_and_unzip(
112116
f"/api/problem/download/problem_alias/{problem_alias}/"
113117
)
114118
parsed_url = urlparse(download_url)
119+
if parsed_url.hostname is None:
120+
LOG.error(f"Invalid download URL (missing hostname): "
121+
f"{download_url}")
122+
return False
115123
conn = http.client.HTTPSConnection(parsed_url.hostname,
116124
context=context)
117125

@@ -191,10 +199,10 @@ def download_and_unzip(
191199

192200
def process_add(
193201
data: Dict[str, Any],
194-
problems_data: Dict[str, List[Dict[str, str]]],
202+
problems_data: Dict[str, List[ProblemEntry]],
195203
client: omegaup.api.Client,
196204
base_url: str
197-
):
205+
) -> None:
198206
for item in data.get("add_problem", []):
199207
course = item["course_alias"]
200208
assignment = item["assignment_alias"]
@@ -238,6 +246,11 @@ def process_add(
238246
os.makedirs(assignment_folder, exist_ok=True)
239247

240248
LOG.info(f"📥 Downloading and unzipping problem '{problem}'")
249+
250+
if client.api_token is None:
251+
LOG.error("❌ API token is required.")
252+
return
253+
241254
success = download_and_unzip(
242255
problem_alias=problem,
243256
assignment_folder=assignment_folder,
@@ -262,9 +275,9 @@ def process_add(
262275

263276
def process_remove(
264277
data: Dict[str, Any],
265-
problems_data: Dict[str, List[Dict[str, str]]],
278+
problems_data: Dict[str, List[ProblemEntry]],
266279
client: omegaup.api.Client
267-
):
280+
) -> None:
268281
for item in data.get("remove_problem", []):
269282
course = item["course_alias"]
270283
assignment = item["assignment_alias"]
@@ -342,7 +355,9 @@ def load_problems_json() -> Dict[str, List[ProblemEntry]]:
342355
return {"problems": []}
343356

344357

345-
def save_problems_json(data: Dict[str, List[ProblemEntry]]):
358+
def save_problems_json(data: Dict[str, List[ProblemEntry]]) -> None:
359+
"""Saves the problems data to the problems.json file."""
360+
346361
with open(PROBLEMS_JSON_PATH, "w", encoding="utf-8") as f:
347362
json.dump(
348363
{"problems": [p._asdict() for p in data["problems"]]},
@@ -352,15 +367,19 @@ def save_problems_json(data: Dict[str, List[ProblemEntry]]):
352367

353368

354369
def add_problem_to_json(course: str, assignment: str, problem: str,
355-
problems_data: Dict[str, List[ProblemEntry]]):
370+
problems_data: Dict[str, List[ProblemEntry]]) -> None:
356371
path = f"Courses/{course}/{assignment}/{problem}"
357372
if not any(p.path == path for p in problems_data["problems"]):
358373
problems_data["problems"].append(ProblemEntry(path=path))
359374
LOG.info(f"📝 Added '{path}' to problems.json")
360375

361376

362-
def remove_problem_from_json(course: str, assignment: str, problem: str,
363-
problems_data: Dict[str, List[ProblemEntry]]):
377+
def remove_problem_from_json(
378+
course: str,
379+
assignment: str,
380+
problem: str,
381+
problems_data: Dict[str, List[ProblemEntry]]
382+
) -> None:
364383
path = f"Courses/{course}/{assignment}/{problem}"
365384
before = len(problems_data["problems"])
366385
problems_data["problems"] = [
@@ -370,7 +389,7 @@ def remove_problem_from_json(course: str, assignment: str, problem: str,
370389
LOG.info(f"🗑️ Removed '{path}' from problems.json")
371390

372391

373-
def main():
392+
def main() -> None:
374393
api_token, base_url, input_path = handle_input()
375394
client = omegaup.api.Client(api_token=api_token, url=base_url)
376395

@@ -401,7 +420,7 @@ def main():
401420
except (IOError, json.JSONDecodeError) as e:
402421
LOG.info(
403422
f"🧹 Cleared 'add_problem' and 'remove_problem' arrays in "
404-
f"{input_path}"
423+
f"{input_path}: {e}"
405424
)
406425

407426

0 commit comments

Comments
 (0)