kattis-cli 1.0.7__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kattis_cli/client.py +466 -0
- kattis_cli/download.py +196 -145
- kattis_cli/fireworks.py +128 -0
- kattis_cli/kattis.py +68 -412
- kattis_cli/kattis_setup.py +124 -87
- kattis_cli/main.py +3 -3
- kattis_cli/solution_tester.py +221 -0
- kattis_cli/ui.py +112 -71
- {kattis_cli-1.0.7.dist-info → kattis_cli-1.1.0.dist-info}/METADATA +28 -20
- kattis_cli-1.1.0.dist-info/RECORD +21 -0
- {kattis_cli-1.0.7.dist-info → kattis_cli-1.1.0.dist-info}/WHEEL +1 -1
- kattis_cli/test_solution.py +0 -179
- kattis_cli-1.0.7.dist-info/RECORD +0 -19
- {kattis_cli-1.0.7.dist-info → kattis_cli-1.1.0.dist-info}/entry_points.txt +0 -0
- {kattis_cli-1.0.7.dist-info → kattis_cli-1.1.0.dist-info/licenses}/LICENSE +0 -0
kattis_cli/client.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Object-oriented Kattis API client.
|
|
4
|
+
|
|
5
|
+
This class wraps login, submit and status-checking functionality previously
|
|
6
|
+
implemented as module-level functions in `kattis.py`.
|
|
7
|
+
"""
|
|
8
|
+
from pdb import run
|
|
9
|
+
from typing import List, Any, final
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
import configparser
|
|
15
|
+
from bs4 import BeautifulSoup
|
|
16
|
+
import requests
|
|
17
|
+
import requests.exceptions
|
|
18
|
+
import requests.cookies
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.align import Align
|
|
21
|
+
from rich.live import Live
|
|
22
|
+
from rich.prompt import Confirm
|
|
23
|
+
import subprocess
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from kattis_cli.utils import languages
|
|
27
|
+
from kattis_cli.utils import config
|
|
28
|
+
from kattis_cli import ui
|
|
29
|
+
from kattis_cli.fireworks import run_fireworks
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KattisClient:
|
|
33
|
+
"""A simple client for interacting with Kattis (login, submit, poll)."""
|
|
34
|
+
|
|
35
|
+
_HEADERS = {'User-Agent': 'kattis-cli-submit'}
|
|
36
|
+
|
|
37
|
+
_RUNNING_STATUS = 5
|
|
38
|
+
_COMPILE_ERROR_STATUS = 8
|
|
39
|
+
_ACCEPTED_STATUS = 16
|
|
40
|
+
|
|
41
|
+
_STATUS_MAP = {
|
|
42
|
+
0: 'New',
|
|
43
|
+
1: 'New',
|
|
44
|
+
2: 'Waiting for compile',
|
|
45
|
+
3: 'Compiling',
|
|
46
|
+
4: 'Waiting for run',
|
|
47
|
+
_RUNNING_STATUS: 'Running',
|
|
48
|
+
6: 'Judge Error',
|
|
49
|
+
7: 'Submission Error',
|
|
50
|
+
_COMPILE_ERROR_STATUS: 'Compile Error',
|
|
51
|
+
9: 'Run Time Error',
|
|
52
|
+
10: 'Memory Limit Exceeded',
|
|
53
|
+
11: 'Output Limit Exceeded',
|
|
54
|
+
12: 'Time Limit Exceeded',
|
|
55
|
+
13: 'Illegal Function',
|
|
56
|
+
14: 'Wrong Answer',
|
|
57
|
+
_ACCEPTED_STATUS: 'Accepted',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self.console = Console()
|
|
62
|
+
|
|
63
|
+
# --- Authentication ---
|
|
64
|
+
def login(self, login_url: str, username: str,
|
|
65
|
+
password: str = '', token: str = '') -> requests.Response:
|
|
66
|
+
"""Log in to Kattis using username and either a password or token.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
login_url: Full URL to the Kattis login endpoint.
|
|
70
|
+
username: Kattis username or email.
|
|
71
|
+
password: Optional password for login.
|
|
72
|
+
token: Optional API token for script-based login.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The requests.Response object from the POST request.
|
|
76
|
+
"""
|
|
77
|
+
login_args = {'user': username, 'script': 'true'}
|
|
78
|
+
if password:
|
|
79
|
+
login_args['password'] = password
|
|
80
|
+
if token:
|
|
81
|
+
login_args['token'] = token
|
|
82
|
+
|
|
83
|
+
return requests.post(
|
|
84
|
+
login_url,
|
|
85
|
+
data=login_args,
|
|
86
|
+
headers=self._HEADERS,
|
|
87
|
+
timeout=10)
|
|
88
|
+
|
|
89
|
+
def login_from_config(
|
|
90
|
+
self,
|
|
91
|
+
cfg: configparser.ConfigParser) -> requests.Response:
|
|
92
|
+
"""Log in using credentials found in a .kattisrc config.
|
|
93
|
+
|
|
94
|
+
Reads username and either password or token from the provided
|
|
95
|
+
ConfigParser and performs a login. Raises ConfigError when the
|
|
96
|
+
configuration is invalid.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
username = cfg.get('user', 'username')
|
|
100
|
+
password = token = ''
|
|
101
|
+
try:
|
|
102
|
+
password = cfg.get('user', 'password')
|
|
103
|
+
except configparser.NoOptionError:
|
|
104
|
+
pass
|
|
105
|
+
try:
|
|
106
|
+
token = cfg.get('user', 'token')
|
|
107
|
+
except configparser.NoOptionError:
|
|
108
|
+
pass
|
|
109
|
+
if not password and not token:
|
|
110
|
+
raise config.ConfigError('''\
|
|
111
|
+
Your .kattisrc file appears corrupted. It must provide a token (or a
|
|
112
|
+
KATTIS password).
|
|
113
|
+
|
|
114
|
+
Please download a new .kattisrc file''')
|
|
115
|
+
|
|
116
|
+
loginurl = self.get_url(cfg, 'loginurl', 'login')
|
|
117
|
+
return self.login(loginurl, username, password, token)
|
|
118
|
+
|
|
119
|
+
def get_url(
|
|
120
|
+
self,
|
|
121
|
+
cfg: configparser.ConfigParser,
|
|
122
|
+
option: str,
|
|
123
|
+
default: str) -> str:
|
|
124
|
+
"""Return a URL taken from config or constructed from hostname.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
cfg: ConfigParser loaded from .kattisrc.
|
|
128
|
+
option: Config option name that might hold the URL.
|
|
129
|
+
default: Default path to append to the hostname if option
|
|
130
|
+
is not present.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
if cfg.has_option('kattis', option):
|
|
134
|
+
return cfg.get('kattis', option)
|
|
135
|
+
else:
|
|
136
|
+
return f"https://{cfg.get('kattis', 'hostname')}/{default}"
|
|
137
|
+
|
|
138
|
+
# --- Submission ---
|
|
139
|
+
def submit(self,
|
|
140
|
+
submit_url: str,
|
|
141
|
+
cookies: requests.cookies.RequestsCookieJar,
|
|
142
|
+
problem: str,
|
|
143
|
+
language: str,
|
|
144
|
+
files: List[str],
|
|
145
|
+
mainclass: str,
|
|
146
|
+
tag: str) -> requests.Response:
|
|
147
|
+
"""Submit a solution to the Kattis submit endpoint.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
submit_url: Full URL to POST the submission to.
|
|
151
|
+
cookies: Requests cookie jar from an authenticated session.
|
|
152
|
+
problem: Problem id to submit to.
|
|
153
|
+
language: Language identifier to use for the submission.
|
|
154
|
+
files: List of file paths to include in the submission.
|
|
155
|
+
mainclass: Main class/file name (if applicable).
|
|
156
|
+
tag: Optional tag to attach to the submission.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The requests.Response returned by the POST.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
data = {'submit': 'true',
|
|
163
|
+
'submit_ctr': 2,
|
|
164
|
+
'language': language,
|
|
165
|
+
'mainclass': mainclass,
|
|
166
|
+
'problem': problem,
|
|
167
|
+
'tag': tag,
|
|
168
|
+
'script': 'true'}
|
|
169
|
+
|
|
170
|
+
sub_files = []
|
|
171
|
+
for f in files:
|
|
172
|
+
with open(f, 'rb') as sub_file:
|
|
173
|
+
sub_files.append(('sub_file[]',
|
|
174
|
+
(os.path.basename(f),
|
|
175
|
+
sub_file.read(),
|
|
176
|
+
'application/octet-stream')))
|
|
177
|
+
|
|
178
|
+
return requests.post(
|
|
179
|
+
submit_url,
|
|
180
|
+
data=data,
|
|
181
|
+
files=sub_files,
|
|
182
|
+
cookies=cookies,
|
|
183
|
+
headers=self._HEADERS, timeout=10)
|
|
184
|
+
|
|
185
|
+
def get_submission_url(self, submit_response: str,
|
|
186
|
+
cfg: configparser.ConfigParser) -> str:
|
|
187
|
+
"""Extract the submission URL from the server submission reply.
|
|
188
|
+
|
|
189
|
+
The Kattis submit reply typically contains a line indicating the
|
|
190
|
+
Submission ID. This builds the full submissions URL from config
|
|
191
|
+
and returns the specific submission URL.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
m = re.search(r'Submission ID: (\d+)', submit_response)
|
|
195
|
+
if m:
|
|
196
|
+
submissions_url = self.get_url(
|
|
197
|
+
cfg, 'submissionsurl', 'submissions')
|
|
198
|
+
submission_id = m.group(1)
|
|
199
|
+
return f'{submissions_url}/{submission_id}'
|
|
200
|
+
else:
|
|
201
|
+
raise config.ConfigError(
|
|
202
|
+
'Could not find submission ID in response')
|
|
203
|
+
|
|
204
|
+
def get_submission_status(
|
|
205
|
+
self,
|
|
206
|
+
submission_url: str,
|
|
207
|
+
cookies: requests.cookies.RequestsCookieJar) -> Any:
|
|
208
|
+
"""Poll the submission status JSON endpoint and return parsed JSON.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
submission_url: Base URL for the submission (without ?json).
|
|
212
|
+
cookies: Authenticated session cookies.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Parsed JSON as returned by the status endpoint.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
reply = requests.get(
|
|
219
|
+
submission_url + '?json',
|
|
220
|
+
cookies=cookies,
|
|
221
|
+
headers=self._HEADERS, timeout=10)
|
|
222
|
+
return reply.json()
|
|
223
|
+
|
|
224
|
+
# --- HTML parsing ---
|
|
225
|
+
def parse_row_html(self, html: str) -> Any:
|
|
226
|
+
"""Parse the HTML snippet for a single submission row.
|
|
227
|
+
|
|
228
|
+
The method extracts runtime, status, language and per-test results
|
|
229
|
+
from the HTML snippet returned by the Kattis submission API.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
runtime = '❓ s'
|
|
233
|
+
status = '❓'
|
|
234
|
+
language = '❓'
|
|
235
|
+
test_status = '❓/❓'
|
|
236
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
237
|
+
tr_submission: Any = soup.find("tr", {"data-submission-id": True})
|
|
238
|
+
if tr_submission:
|
|
239
|
+
td_cputime = tr_submission.find("td", {"data-type": "cpu"})
|
|
240
|
+
if td_cputime:
|
|
241
|
+
runtime = td_cputime.text.strip().replace(' ', ' ')
|
|
242
|
+
if not runtime:
|
|
243
|
+
runtime = '❓ s'
|
|
244
|
+
div_status = tr_submission.find(
|
|
245
|
+
"div", {"class": "status"}, recursive=True)
|
|
246
|
+
if div_status:
|
|
247
|
+
status = div_status.text.strip()
|
|
248
|
+
else:
|
|
249
|
+
status = '❓'
|
|
250
|
+
td_lang = tr_submission.find(
|
|
251
|
+
"td", {"data-type": "lang"}, recursive=False)
|
|
252
|
+
if td_lang:
|
|
253
|
+
language = td_lang.text.strip()
|
|
254
|
+
else:
|
|
255
|
+
language = '❓'
|
|
256
|
+
td_test_cases = tr_submission.find(
|
|
257
|
+
"td", {"data-type": "testcases"})
|
|
258
|
+
if td_test_cases:
|
|
259
|
+
test_status = td_test_cases.text.strip()
|
|
260
|
+
|
|
261
|
+
i_tag = soup.find_all("i", {"class": True})
|
|
262
|
+
test_result = []
|
|
263
|
+
for num, i in enumerate(i_tag):
|
|
264
|
+
if 'title' not in i.attrs:
|
|
265
|
+
continue
|
|
266
|
+
title = i['title'].split(':')
|
|
267
|
+
if 'Accepted' in title[-1]:
|
|
268
|
+
test_result.append(f'{num}✅')
|
|
269
|
+
elif 'not checked' in title[-1]:
|
|
270
|
+
test_result.append(f'{num}❓')
|
|
271
|
+
else:
|
|
272
|
+
test_result.append(f'{num}❌')
|
|
273
|
+
return runtime, status, language, test_status, test_result
|
|
274
|
+
|
|
275
|
+
# --- High level flows ---
|
|
276
|
+
def show_kattis_judgement(self, problemid: str, submission_url: str,
|
|
277
|
+
cfg: configparser.ConfigParser) -> None:
|
|
278
|
+
"""Display a live view of the Kattis judgement for a submission.
|
|
279
|
+
|
|
280
|
+
This will poll the Kattis status endpoint and render a live
|
|
281
|
+
textual UI until the submission reaches a final state.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
config_data = ui.show_problem_metadata(problemid)
|
|
285
|
+
config_data['submissions'] += 1
|
|
286
|
+
console = Console()
|
|
287
|
+
login_reply = self.login_from_config(cfg)
|
|
288
|
+
title = '\n[bold blue] '
|
|
289
|
+
title += ':cat: Kattis Judgement Results :cat:[/]\n'
|
|
290
|
+
status_id = 0
|
|
291
|
+
with Live(console=console, screen=False,
|
|
292
|
+
refresh_per_second=10) as kattis_live:
|
|
293
|
+
|
|
294
|
+
while True:
|
|
295
|
+
time.sleep(0.1)
|
|
296
|
+
result = self.get_submission_status(submission_url,
|
|
297
|
+
login_reply.cookies)
|
|
298
|
+
rt, status, lang, t_status, t_results = self.parse_row_html(
|
|
299
|
+
result['row_html'])
|
|
300
|
+
status_id = int(result['status_id'])
|
|
301
|
+
if status_id > 4 and status_id < 16:
|
|
302
|
+
judgement = f'[bold red] {status}[/]'
|
|
303
|
+
else:
|
|
304
|
+
judgement = f'[bold yellow] {status}[/]'
|
|
305
|
+
if status_id == 12:
|
|
306
|
+
runtime = f'[bold red] {rt}[/]'
|
|
307
|
+
else:
|
|
308
|
+
runtime = f'[bold yellow] {rt}[/]'
|
|
309
|
+
|
|
310
|
+
text = title + f'\n[bold blue]JUDGEMENT:[/] {judgement}'
|
|
311
|
+
text += f'\t[bold blue]LANGUAGE:[/] [bold yellow]{lang}[/]'
|
|
312
|
+
text += f'\t[bold blue]RUNTIME:[/] {runtime}'
|
|
313
|
+
text += f'\t[bold deep_pink3][link={submission_url}]'
|
|
314
|
+
text += 'VIEW DETAILS ON KATTIS[/link][/]\n\n'
|
|
315
|
+
text += (f'[bold blue]TESTCASES:[/] '
|
|
316
|
+
f'[bold green]{t_status}[/]\n\n')
|
|
317
|
+
if status_id < 5:
|
|
318
|
+
test_cases = (
|
|
319
|
+
'🤞🏻🤞🏻🤞🏻🤞🏻🤞🏻 '
|
|
320
|
+
'[bold yellow]WAITING...[/] '
|
|
321
|
+
'🤞🏻🤞🏻🤞🏻🤞🏻🤞🏻\u200d'
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
test_cases = ' '.join(t_results) + '\n'
|
|
325
|
+
text += test_cases
|
|
326
|
+
kattis_live.update(Align.center(text))
|
|
327
|
+
if status_id > 5:
|
|
328
|
+
kattis_live.stop()
|
|
329
|
+
break
|
|
330
|
+
if status_id == self._ACCEPTED_STATUS:
|
|
331
|
+
verdict = '👍🎆🔥🎈🎈 [bold yellow]YAY!! KEEP GOING...[/] 🎈🎈👍🎆🔥'
|
|
332
|
+
console.print()
|
|
333
|
+
config_data['accepted'] += 1
|
|
334
|
+
# Try to launch an external fireworks script in a separate
|
|
335
|
+
# process. This keeps GUI code out of the main client process
|
|
336
|
+
# and avoids import-time side effects.
|
|
337
|
+
try:
|
|
338
|
+
run_fireworks()
|
|
339
|
+
except Exception:
|
|
340
|
+
# Best-effort: failures to launch fireworks should not
|
|
341
|
+
# affect normal client operation.
|
|
342
|
+
pass
|
|
343
|
+
else:
|
|
344
|
+
verdict = '💪🧐💪 [bold green]SORRY![/] 🧐💪🧐'
|
|
345
|
+
console.print(Align.center(verdict))
|
|
346
|
+
config.update_problem_metadata(problemid, config_data)
|
|
347
|
+
ui.show_problem_metadata(problemid)
|
|
348
|
+
|
|
349
|
+
def get_login_reply(
|
|
350
|
+
self,
|
|
351
|
+
cfg: configparser.ConfigParser) -> requests.Response:
|
|
352
|
+
"""Attempt login using config and handle common error modes.
|
|
353
|
+
|
|
354
|
+
Returns the successful login Response or exits the process when
|
|
355
|
+
login cannot be completed.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
login_reply = self.login_from_config(cfg)
|
|
360
|
+
except config.ConfigError as exc:
|
|
361
|
+
self.console.print(exc)
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
except requests.exceptions.RequestException as err:
|
|
364
|
+
self.console.print('Login connection failed:', err)
|
|
365
|
+
sys.exit(1)
|
|
366
|
+
|
|
367
|
+
if not login_reply.status_code == 200:
|
|
368
|
+
self.console.print('Login failed.')
|
|
369
|
+
if login_reply.status_code == 403:
|
|
370
|
+
self.console.print(
|
|
371
|
+
'Incorrect username or password/token (403)')
|
|
372
|
+
elif login_reply.status_code == 404:
|
|
373
|
+
self.console.print('Incorrect login URL (404)')
|
|
374
|
+
else:
|
|
375
|
+
self.console.print('Status code:', login_reply.status_code)
|
|
376
|
+
sys.exit(1)
|
|
377
|
+
return login_reply
|
|
378
|
+
|
|
379
|
+
def submit_solution(self, files: List[str], problemid: str,
|
|
380
|
+
language: str, mainclass: str,
|
|
381
|
+
tag: str, force: bool) -> None:
|
|
382
|
+
"""High-level helper to submit a solution from file paths.
|
|
383
|
+
|
|
384
|
+
This wraps login-from-config, optional confirmation UI and the
|
|
385
|
+
lower-level submit call. On success it may follow-up by showing
|
|
386
|
+
the live judgement UI.
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
cfg = config.get_kattisrc()
|
|
391
|
+
except config.ConfigError as exc:
|
|
392
|
+
self.console.print(exc)
|
|
393
|
+
sys.exit(1)
|
|
394
|
+
login_reply = self.get_login_reply(cfg)
|
|
395
|
+
if not files:
|
|
396
|
+
self.console.print('No files specified')
|
|
397
|
+
sys.exit(1)
|
|
398
|
+
|
|
399
|
+
files = sorted(list(set(files)))
|
|
400
|
+
|
|
401
|
+
submit_url = self.get_url(cfg, 'submissionurl', 'submit')
|
|
402
|
+
|
|
403
|
+
if not force:
|
|
404
|
+
# reuse module-level confirm UI
|
|
405
|
+
self._confirm_or_die(problemid, language, files, mainclass, tag)
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
result = self.submit(submit_url,
|
|
409
|
+
login_reply.cookies,
|
|
410
|
+
problemid,
|
|
411
|
+
language,
|
|
412
|
+
files,
|
|
413
|
+
mainclass,
|
|
414
|
+
tag)
|
|
415
|
+
except requests.exceptions.RequestException as err:
|
|
416
|
+
self.console.print('Submit connection failed:',
|
|
417
|
+
err, style='bold red')
|
|
418
|
+
sys.exit(1)
|
|
419
|
+
|
|
420
|
+
if result.status_code != 200:
|
|
421
|
+
self.console.print('Submission failed.', style='bold red')
|
|
422
|
+
if result.status_code == 403:
|
|
423
|
+
self.console.print('Access denied (403)', style='bold red')
|
|
424
|
+
elif result.status_code == 404:
|
|
425
|
+
self.console.print('Incorrect submit URL (404)')
|
|
426
|
+
else:
|
|
427
|
+
self.console.print(
|
|
428
|
+
'Status code:', result.status_code, style='bold red')
|
|
429
|
+
sys.exit(1)
|
|
430
|
+
|
|
431
|
+
plain_result = result.content.decode('utf-8').replace('<br />', '\n')
|
|
432
|
+
self.console.print(f'[bold blue]{plain_result}[/]')
|
|
433
|
+
|
|
434
|
+
submission_url = None
|
|
435
|
+
try:
|
|
436
|
+
submission_url = self.get_submission_url(plain_result, cfg)
|
|
437
|
+
except configparser.NoOptionError:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
if submission_url:
|
|
441
|
+
self.show_kattis_judgement(problemid, submission_url, cfg)
|
|
442
|
+
|
|
443
|
+
def _confirm_or_die(self, problem: str, language: str,
|
|
444
|
+
files: List[str], mainclass: str,
|
|
445
|
+
tag: str) -> None:
|
|
446
|
+
"""Ask the user to confirm the submission or exit the program.
|
|
447
|
+
|
|
448
|
+
This uses the rich Confirm prompt to request user confirmation
|
|
449
|
+
before submitting to Kattis.
|
|
450
|
+
"""
|
|
451
|
+
|
|
452
|
+
console = Console()
|
|
453
|
+
console.clear()
|
|
454
|
+
console.print('Problem:', problem)
|
|
455
|
+
console.print('Language:', language)
|
|
456
|
+
console.print('Files:', ', '.join(files))
|
|
457
|
+
if mainclass:
|
|
458
|
+
if language in languages.GUESS_MAINFILE:
|
|
459
|
+
console.print('Main file:', mainclass)
|
|
460
|
+
else:
|
|
461
|
+
console.print('Mainclass:', mainclass)
|
|
462
|
+
if tag:
|
|
463
|
+
console.print('Tag:', tag)
|
|
464
|
+
if not Confirm.ask('Submit to Kattis', default=True):
|
|
465
|
+
console.print('Cancelling...')
|
|
466
|
+
sys.exit(1)
|