Skip to content

Commit 714de35

Browse files
authored
Merge pull request #65 from Juanito98/deploy_contests
Deploy contests
2 parents 0940ed0 + b0d8d9e commit 714de35

File tree

10 files changed

+660
-130
lines changed

10 files changed

+660
-130
lines changed

Pipfile

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
[[source]]
2-
32
url = "https://pypi.python.org/simple"
43
verify_ssl = true
54
name = "pypi"
65

7-
86
[dev-packages]
9-
107
mypy = ">=0.782"
118
pycodestyle = ">=2.6.0"
12-
9+
types-PyYAML = ">=6.0.12.20"
1310

1411
[packages]
15-
1612
libkarel = ">=1.0.2"
17-
omegaup = ">=1.3.0"
18-
13+
omegaup = "==1.3.0"
14+
pyyaml = ">=6.0.1"
1915

2016
[requires]
21-
2217
python_version = "3.8"

Pipfile.lock

Lines changed: 217 additions & 85 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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 Any, 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[Any]]]:
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.

contests.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import os
2+
import logging
3+
from typing import (
4+
NamedTuple,
5+
Mapping,
6+
Any,
7+
Sequence,
8+
List,
9+
Optional,
10+
Dict,
11+
Set,
12+
)
13+
import omegaup.api
14+
import repository
15+
import json
16+
import datetime
17+
import yaml
18+
19+
_CONFIG_FILE = 'contest.yaml'
20+
21+
22+
class Contest(NamedTuple):
23+
"""Represents a single contest."""
24+
path: str
25+
title: str
26+
config: Mapping[str, Any]
27+
28+
@staticmethod
29+
def load(contestPath: str, rootDirectory: str) -> 'Contest':
30+
"""Load a single contest from the path."""
31+
with open(os.path.join(rootDirectory, contestPath, _CONFIG_FILE)) as f:
32+
problemConfig = yaml.safe_load(f)
33+
34+
return Contest(path=contestPath,
35+
title=problemConfig['title'],
36+
config=problemConfig)
37+
38+
39+
def contests(allContests: bool = False,
40+
contestPaths: Sequence[str] = (),
41+
rootDirectory: Optional[str] = None) -> List[Contest]:
42+
"""Gets the list of contests that will be considered.
43+
44+
If `allContests` is passed, all the contests that are declared in
45+
`contests.json` will be returned. Otherwise, only those that have
46+
differences with `upstream/main`.
47+
"""
48+
if rootDirectory is None:
49+
rootDirectory = repository.repositoryRoot()
50+
51+
logging.info('Loading contests...')
52+
53+
if contestPaths:
54+
# Generate the Contest objects from just the path. The title is ignored
55+
# anyways, since it's read from the configuration file in the contest
56+
# directory for anything important.
57+
return [
58+
Contest.load(contestPath=contestPath, rootDirectory=rootDirectory)
59+
for contestPath in contestPaths
60+
]
61+
62+
with open(os.path.join(rootDirectory, 'problems.json'), 'r') as p:
63+
config = json.load(p)
64+
65+
configContests: List[Contest] = []
66+
for contest in config['contests']:
67+
if contest.get('disabled', False):
68+
logging.warning('Contest %s disabled. Skipping.', contest['title'])
69+
continue
70+
configContests.append(
71+
Contest.load(contestPath=contest['path'],
72+
rootDirectory=rootDirectory))
73+
74+
if allContests:
75+
logging.info('Loading everything as requested.')
76+
return configContests
77+
78+
changes = repository.gitDiff(rootDirectory)
79+
80+
contests: List[Contest] = []
81+
for contest in configContests:
82+
logging.info('Loading %s.', contest.title)
83+
84+
if contest.path not in changes:
85+
logging.info('No changes to %s. Skipping.', contest.title)
86+
continue
87+
contests.append(contest)
88+
89+
return contests
90+
91+
92+
def date_to_timestamp(date: str) -> int:
93+
return int(
94+
datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%SZ').timestamp())
95+
96+
97+
def upsertContest(
98+
client: omegaup.api.Client,
99+
contestPath: str,
100+
canCreate: bool,
101+
timeout: datetime.timedelta,
102+
) -> None:
103+
"""Upsert a contest to omegaUp given the configuration."""
104+
with open(os.path.join(contestPath, _CONFIG_FILE)) as f:
105+
contestConfig = yaml.safe_load(f)
106+
107+
logging.info('Upserting contest %s...', contestConfig['title'])
108+
109+
title = contestConfig['title']
110+
alias = contestConfig['alias']
111+
misc = contestConfig['misc']
112+
languages = misc['languages']
113+
114+
if languages == 'all':
115+
misc['languages'] = ','.join((
116+
'c11-clang',
117+
'c11-gcc',
118+
'cpp11-clang',
119+
'cpp11-gcc',
120+
'cpp17-clang',
121+
'cpp17-gcc',
122+
'cpp20-clang',
123+
'cpp20-gcc',
124+
'cs',
125+
'go',
126+
'hs',
127+
'java',
128+
'js',
129+
'kt',
130+
'lua',
131+
'pas',
132+
'py2',
133+
'py3',
134+
'rb',
135+
'rs',
136+
))
137+
elif languages == 'karel':
138+
misc['languages'] = 'kj,kp'
139+
elif languages == 'none':
140+
misc['languages'] = ''
141+
142+
payload = {
143+
'title': title,
144+
'admission_mode': misc['admission_mode'],
145+
'description': contestConfig.get('description', ''),
146+
'feedback': misc["feedback"],
147+
'finish_time': date_to_timestamp(contestConfig['finish_time']),
148+
'languages': misc['languages'],
149+
'penalty': misc['penalty']['time'],
150+
'penalty_calc_policy': misc['penalty']['calc_policy'],
151+
'penalty_type': misc['penalty']['type'],
152+
'points_decay_factor': misc['penalty']['points_decay_factor'],
153+
'requests_user_information': str(misc['requests_user_information']),
154+
'score_mode': misc['score_mode'],
155+
'scoreboard': misc['scoreboard'],
156+
'show_scoreboard_after': misc['show_scoreboard_after'],
157+
'submissions_gap': misc['submissions_gap'],
158+
'start_time': date_to_timestamp(contestConfig['start_time']),
159+
'window_length': contestConfig.get('window_length', None),
160+
}
161+
162+
exists = client.contest.details(contest_alias=alias,
163+
check_=False)["status"] == 'ok'
164+
165+
if not exists:
166+
if not canCreate:
167+
raise Exception("Contest doesn't exist!")
168+
logging.info("Contest doesn't exist. Creating contest.")
169+
endpoint = '/api/contest/create/'
170+
payload['alias'] = alias
171+
else:
172+
endpoint = '/api/contest/update/'
173+
payload['contest_alias'] = alias
174+
175+
client.query(endpoint, payload, timeout_=timeout)
176+
177+
# Adding admins
178+
targetAdmins: Sequence[str] = contestConfig.get('admins',
179+
{}).get('users', [])
180+
targetAdminGroups: Sequence[str] = contestConfig.get('admins',
181+
{}).get('groups', [])
182+
183+
allAdmins = client.contest.admins(contest_alias=alias)
184+
185+
if len(targetAdmins) > 0:
186+
admins = {
187+
a['username'].lower()
188+
for a in allAdmins['admins'] if a['role'] == 'admin'
189+
}
190+
191+
desiredAdmins = {admin.lower() for admin in targetAdmins}
192+
193+
clientAdmin: Set[str] = set()
194+
if client.username:
195+
clientAdmin.add(client.username.lower())
196+
adminsToRemove = admins - desiredAdmins - clientAdmin
197+
adminsToAdd = desiredAdmins - admins - clientAdmin
198+
199+
for admin in adminsToAdd:
200+
logging.info('Adding contest admin: %s', admin)
201+
client.contest.addAdmin(contest_alias=alias, usernameOrEmail=admin)
202+
203+
for admin in adminsToRemove:
204+
logging.info('Removing contest admin: %s', admin)
205+
client.contest.removeAdmin(contest_alias=alias,
206+
usernameOrEmail=admin)
207+
208+
adminGroups = {
209+
a['alias'].lower()
210+
for a in allAdmins['group_admins'] if a['role'] == 'admin'
211+
}
212+
213+
desiredGroups = {group.lower() for group in targetAdminGroups}
214+
215+
groupsToRemove = adminGroups - desiredGroups
216+
groupsToAdd = desiredGroups - adminGroups
217+
218+
for group in groupsToAdd:
219+
logging.info('Adding contest admin group: %s', group)
220+
client.contest.addGroupAdmin(contest_alias=alias, group=group)
221+
222+
for group in groupsToRemove:
223+
logging.info('Removing contest admin group: %s', group)
224+
client.contest.removeGroupAdmin(contest_alias=alias, group=group)
225+
226+
# Adding problems
227+
targetProblems: Sequence[Dict[str,
228+
Any]] = contestConfig.get('problems', [])
229+
230+
allProblems = client.contest.problems(contest_alias=alias)
231+
problems = {
232+
p['alias'].lower(): {
233+
'points': p['points'],
234+
'order_in_contest': p['order'],
235+
}
236+
for p in allProblems['problems']
237+
}
238+
239+
desiredProblems = {
240+
problem['alias'].lower(): {
241+
'points': problem.get('points', 100),
242+
'order_in_contest': problem.get('order_in_contest', idx + 1),
243+
}
244+
for idx, problem in enumerate(targetProblems)
245+
}
246+
problemsToRemove = problems.keys() - desiredProblems
247+
problemsToUpsert = (desiredProblems.keys() - problems.keys()) | {
248+
problem
249+
for problem in problems
250+
if problem in problems and problem in desiredProblems and
251+
(desiredProblems[problem] != problems[problem])
252+
}
253+
254+
for problem in problemsToUpsert:
255+
logging.info('Upserting contest problem: %s', problem)
256+
client.contest.addProblem(
257+
contest_alias=alias,
258+
problem_alias=problem,
259+
order_in_contest=desiredProblems[problem]['order_in_contest'],
260+
points=desiredProblems[problem]['points'])
261+
262+
for problem in problemsToRemove:
263+
logging.info('Removing contest problem: %s', problem)
264+
client.contest.removeProblem(contest_alias=alias,
265+
problem_alias=problem)
266+
267+
# Adding contestants
268+
targetContestants: Sequence[str] = contestConfig.get('contestants',
269+
{}).get('users', [])
270+
targetContestantGroups: Sequence[str] = contestConfig.get(
271+
'contestants', {}).get('groups', [])
272+
273+
allContestants = client.contest.users(contest_alias=alias)
274+
275+
if len(targetContestants) > 0:
276+
contestants = {c['username'].lower() for c in allContestants['users']}
277+
278+
desiredContestants = {
279+
contestant.lower()
280+
for contestant in targetContestants
281+
}
282+
283+
contestantsToRemove = contestants - desiredContestants
284+
contestantsToAdd = desiredContestants - contestants
285+
286+
for contestant in contestantsToAdd:
287+
logging.info('Adding contestant: %s', contestant)
288+
client.contest.addUser(contest_alias=alias,
289+
usernameOrEmail=contestant)
290+
291+
for contestant in contestantsToRemove:
292+
logging.info('Removing contestant: %s', contestant)
293+
client.contest.removeUser(contest_alias=alias,
294+
usernameOrEmail=contestant)
295+
296+
contestantGroups = {c['alias'].lower() for c in allContestants['groups']}
297+
298+
desiredGroups = {group.lower() for group in targetContestantGroups}
299+
300+
groupsToRemove = contestantGroups - desiredGroups
301+
groupsToAdd = desiredGroups - contestantGroups
302+
303+
for group in groupsToAdd:
304+
logging.info('Adding contestant group: %s', group)
305+
client.contest.addGroup(contest_alias=alias, group=group)
306+
307+
for group in groupsToRemove:
308+
logging.info('Removing contestant group: %s', group)
309+
client.contest.removeGroup(contest_alias=alias, group=group)
310+
311+
logging.info("Successfully upserted contest %s", title)

generateresources.py

Lines changed: 2 additions & 2 deletions
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
@@ -13,6 +12,7 @@
1312

1413
import container
1514
import problems
15+
import repository
1616

1717
_SUPPORTED_GENERATORS = frozenset(('png', 'testplan'))
1818

@@ -200,7 +200,7 @@ def _main() -> None:
200200
level=logging.DEBUG if args.verbose else logging.INFO)
201201
logging.getLogger('urllib3').setLevel(logging.CRITICAL)
202202

203-
rootDirectory = problems.repositoryRoot()
203+
rootDirectory = repository.repositoryRoot()
204204

205205
with concurrent.futures.ThreadPoolExecutor(
206206
max_workers=args.jobs) as executor:

0 commit comments

Comments
 (0)