Skip to content

Commit 6f1bb8c

Browse files
committed
WIP Add contest-export script
1 parent b281c12 commit 6f1bb8c

File tree

2 files changed

+143
-1
lines changed

2 files changed

+143
-1
lines changed

misc-tools/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ TARGETS =
1010
OBJECTS =
1111

1212
SUBST_DOMSERVER = fix_permissions configure-domjudge dj_utils.py \
13-
import-contest force-passwords
13+
import-contest export-contest force-passwords
1414

1515
SUBST_JUDGEHOST = dj_make_chroot dj_run_chroot dj_make_chroot_docker \
1616
dj_judgehost_cleanup

misc-tools/export-contest.in

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/usr/bin/env python3
2+
3+
'''
4+
export-contest -- Convenience script to export a contest (including metadata,
5+
teams and problems) from the command line. Defaults to using the CLI interface;
6+
Specify a DOMjudge API URL as to use that.
7+
8+
Reads credentials from ~/.netrc when using the API.
9+
10+
Part of the DOMjudge Programming Contest Jury System and licensed
11+
under the GNU GPL. See README and COPYING for details.
12+
'''
13+
14+
import json
15+
import sys
16+
from concurrent.futures import ThreadPoolExecutor, as_completed
17+
from pathlib import Path
18+
19+
sys.path.append('@domserver_libdir@')
20+
import dj_utils
21+
22+
mime_to_extension = {
23+
'application/pdf': 'pdf',
24+
'application/zip': 'zip',
25+
'image/jpeg': 'jpg',
26+
'image/png': 'png',
27+
'image/svg+xml': 'svg',
28+
'text/plain': 'txt',
29+
'video/mp4': 'mp4',
30+
'video/mpeg': 'mpg',
31+
'video/webm': 'webm',
32+
}
33+
34+
35+
def usage():
36+
print(f'Usage: {sys.argv[0]} [<domjudge-api-url>]')
37+
exit(1)
38+
39+
40+
def download_file(file: dict, dir: str, default_name: str):
41+
print(f"Downloading '{file['href']}'")
42+
Path(dir).mkdir(parents=True, exist_ok=True)
43+
filename = file['filename'] if 'filename' in file else default_name
44+
dj_utils.do_api_request(file['href'], decode=False, output_file=f'{dir}/{filename}')
45+
46+
47+
def is_file(data) -> bool:
48+
'''
49+
Check whether API data represents a FILE object. This is heuristic because
50+
no property is strictly required, but we need at least `href` to download
51+
the file, so if also we find one other property, we announce a winner.
52+
'''
53+
if not isinstance(data, dict):
54+
return false
55+
return 'href' in data and ('mime' in data or 'filename' in data or 'hash' in data)
56+
57+
58+
files_to_download = []
59+
60+
def recurse_find_files(data, store_path: str, default_name: str):
61+
if isinstance(data, list):
62+
# Special case single element list for simpler default_name
63+
if len(data) == 1:
64+
recurse_find_files(data[0], store_path, default_name)
65+
else:
66+
for i, item in enumerate(data):
67+
recurse_find_files(item, store_path, f"{default_name}.{i}")
68+
elif isinstance(data, dict):
69+
if is_file(data):
70+
if 'mime' in data and data['mime'] in mime_to_extension:
71+
default_name += '.' + mime_to_extension[data['mime']]
72+
files_to_download.append((data, store_path, default_name))
73+
else:
74+
for key, item in data.items():
75+
recurse_find_files(item, store_path, f"{default_name}.{key}")
76+
77+
78+
def download_endpoint(name: str, path: str):
79+
ext = '.ndjson' if name == 'event-feed' else '.json'
80+
filename = name + ext
81+
82+
print(f"Fetching '{path}' to '{filename}'")
83+
data = dj_utils.do_api_request(path, decode=False)
84+
with open(filename, 'wb') as f:
85+
f.write(data)
86+
87+
if ext == '.json':
88+
data = json.loads(data)
89+
store_path = name
90+
if isinstance(data, list):
91+
for elem in data:
92+
recurse_find_files(elem, f"{store_path}/{elem['id']}", '')
93+
else:
94+
recurse_find_files(data, store_path, '')
95+
96+
97+
if len(sys.argv) == 2:
98+
dj_utils.domjudge_api_url = sys.argv[1]
99+
elif len(sys.argv) != 1:
100+
usage()
101+
102+
103+
user_data = dj_utils.do_api_request('user')
104+
if 'admin' not in user_data['roles']:
105+
print('Your user does not have the \'admin\' role, can not export.')
106+
exit(1)
107+
108+
109+
contest_id = 'wf48_finals'
110+
contest_path = f'contests/{contest_id}'
111+
112+
# Custom endpoints:
113+
download_endpoint('api', '')
114+
download_endpoint('contest', contest_path)
115+
download_endpoint('event-feed', f'{contest_path}/event-feed?stream=false')
116+
117+
for endpoint in [
118+
'access',
119+
'accounts',
120+
'awards',
121+
# 'balloons', This is a DOMjudge specific endpoint
122+
'clarifications',
123+
# 'commentary', Not implemented in DOMjudge
124+
'groups',
125+
'judgement-types',
126+
'judgements',
127+
'languages',
128+
'organizations',
129+
# 'persons', Not implemented in DOMjudge
130+
'problems',
131+
'runs',
132+
'scoreboard',
133+
'state',
134+
'submissions',
135+
'teams',
136+
]:
137+
download_endpoint(endpoint, f"{contest_path}/{endpoint}")
138+
139+
with ThreadPoolExecutor(20) as executor:
140+
futures = [executor.submit(download_file, *item) for item in files_to_download]
141+
for future in as_completed(futures):
142+
future.result() # So it can throw any exception

0 commit comments

Comments
 (0)