55import logging
66import os
77import zipfile
8- from typing import Dict , Any
8+ from typing import Dict , Any , List , NamedTuple , cast
99from urllib .parse import urlparse , urljoin
1010import omegaup .api
1111import shutil
1818logging .basicConfig (level = logging .INFO )
1919LOG = logging .getLogger (__name__ )
2020
21- API_CLIENT = None
22- BASE_URL = None
2321
2422# 👇 Add your course aliases here
2523COURSE_ALIASES = [
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
5665def sanitize_filename (name : str ) -> str :
5766 return "" .join (c for c in name if c .isalnum () or c in " -_" ).strip ()
5867
5968
6069def 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" ,
0 commit comments