Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f55ed07

Browse files
committed
Add contest-export script
1 parent 62643cb commit f55ed07

File tree

2 files changed

+191
-1
lines changed

2 files changed

+191
-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: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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 datetime
15+
import json
16+
import os
17+
import sys
18+
import time
19+
from argparse import ArgumentParser
20+
from concurrent.futures import ThreadPoolExecutor, as_completed
21+
22+
sys.path.append('@domserver_libdir@')
23+
import dj_utils
24+
25+
mime_to_extension = {
26+
'application/pdf': 'pdf',
27+
'application/zip': 'zip',
28+
'image/jpeg': 'jpg',
29+
'image/png': 'png',
30+
'image/svg+xml': 'svg',
31+
'text/plain': 'txt',
32+
'video/mp4': 'mp4',
33+
'video/mpeg': 'mpg',
34+
'video/webm': 'webm',
35+
}
36+
37+
def get_default_contest():
38+
c_default = None
39+
40+
contests = dj_utils.do_api_request('contests')
41+
if len(contests)>0:
42+
now = int(time.time())
43+
for c in contests:
44+
if 'start_time' not in c or c['start_time'] is None:
45+
# Assume that a contest with start time unset will start soon.
46+
c['start_epoch'] = now + 1
47+
else:
48+
c['start_epoch'] = datetime.datetime.fromisoformat(c['start_time']).timestamp()
49+
50+
c_default = contests[0]
51+
for c in contests:
52+
if c_default['start_epoch']<=now:
53+
if c['start_epoch']<=now and c['start_epoch']>c_default['start_epoch']:
54+
c_default = c
55+
else:
56+
if c['start_epoch']<c_default['start_epoch']:
57+
c_default = c
58+
59+
return c_default
60+
61+
62+
def download_file(file: dict, dir: str, default_name: str):
63+
print(f"Downloading '{file['href']}'")
64+
os.makedirs(dir, exist_ok=True)
65+
filename = file['filename'] if 'filename' in file else default_name
66+
dj_utils.do_api_request(file['href'], decode=False, output_file=f'{dir}/{filename}')
67+
68+
69+
def is_file(data) -> bool:
70+
'''
71+
Check whether API data represents a FILE object. This is heuristic because
72+
no property is strictly required, but we need at least `href` to download
73+
the file, so if also we find one other property, we announce a winner.
74+
'''
75+
if not isinstance(data, dict):
76+
return false
77+
return 'href' in data and ('mime' in data or 'filename' in data or 'hash' in data)
78+
79+
80+
files_to_download = []
81+
82+
def recurse_find_files(data, store_path: str, default_name: str):
83+
if isinstance(data, list):
84+
# Special case single element list for simpler default_name
85+
if len(data) == 1:
86+
recurse_find_files(data[0], store_path, default_name)
87+
else:
88+
for i, item in enumerate(data):
89+
recurse_find_files(item, store_path, f"{default_name}.{i}")
90+
elif isinstance(data, dict):
91+
if is_file(data):
92+
if 'mime' in data and data['mime'] in mime_to_extension:
93+
default_name += '.' + mime_to_extension[data['mime']]
94+
files_to_download.append((data, store_path, default_name))
95+
else:
96+
for key, item in data.items():
97+
recurse_find_files(item, store_path, f"{default_name}.{key}")
98+
99+
100+
def download_endpoint(name: str, path: str):
101+
ext = '.ndjson' if name == 'event-feed' else '.json'
102+
filename = name + ext
103+
104+
print(f"Fetching '{path}' to '{filename}'")
105+
data = dj_utils.do_api_request(path, decode=False)
106+
with open(filename, 'wb') as f:
107+
f.write(data)
108+
109+
if ext == '.json':
110+
data = json.loads(data)
111+
store_path = name
112+
if isinstance(data, list):
113+
for elem in data:
114+
recurse_find_files(elem, f"{store_path}/{elem['id']}", '')
115+
else:
116+
recurse_find_files(data, store_path, '')
117+
118+
119+
cid = None
120+
dir = None
121+
122+
parser = ArgumentParser(description='Export a contest archive from DOMjudge via the API.')
123+
parser.add_argument('-c', '--cid', help="contest ID to export, defaults to last started, or else first non-started active contest")
124+
parser.add_argument('-d', '--dir', help="directory to write the contest archive to, defaults to contest ID in current directory")
125+
parser.add_argument('-u', '--url', help="DOMjudge API URL to use, if not specified use the CLI interface")
126+
args = parser.parse_args()
127+
128+
if args.cid:
129+
cid = args.cid
130+
else:
131+
c = get_default_contest()
132+
if c is None:
133+
print("No contest specified nor an active contest found.")
134+
exit(1)
135+
else:
136+
cid = c['id']
137+
138+
if args.dir:
139+
dir = args.dir
140+
else:
141+
dir = cid
142+
143+
if args.url:
144+
dj_utils.domjudge_api_url = args.url
145+
146+
user_data = dj_utils.do_api_request('user')
147+
if 'admin' not in user_data['roles']:
148+
print('Your user does not have the \'admin\' role, can not export.')
149+
exit(1)
150+
151+
if os.path.exists(dir):
152+
print(f'Export directory \'{dir}\' already exists, will not overwrite.')
153+
exit(1)
154+
155+
os.makedirs(dir)
156+
os.chdir(dir)
157+
158+
contest_path = f'contests/{cid}'
159+
160+
# Custom endpoints:
161+
download_endpoint('api', '')
162+
download_endpoint('contest', contest_path)
163+
download_endpoint('event-feed', f'{contest_path}/event-feed?stream=false')
164+
165+
for endpoint in [
166+
'access',
167+
'accounts',
168+
'awards',
169+
# 'balloons', This is a DOMjudge specific endpoint
170+
'clarifications',
171+
# 'commentary', Not implemented in DOMjudge
172+
'groups',
173+
'judgement-types',
174+
'judgements',
175+
'languages',
176+
'organizations',
177+
# 'persons', Not implemented in DOMjudge
178+
'problems',
179+
'runs',
180+
'scoreboard',
181+
'state',
182+
'submissions',
183+
'teams',
184+
]:
185+
download_endpoint(endpoint, f"{contest_path}/{endpoint}")
186+
187+
with ThreadPoolExecutor(20) as executor:
188+
futures = [executor.submit(download_file, *item) for item in files_to_download]
189+
for future in as_completed(futures):
190+
future.result() # So it can throw any exception

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /