kattis-cli 1.0.7__py3-none-any.whl → 1.1.1__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/kattis.py CHANGED
@@ -1,433 +1,89 @@
1
- #!/usr/bin/env python
2
- """
3
- Module for submitting solutions to Kattis
4
- https://github.com/Kattis/kattis-cli/blob/master/submit.py
1
+ """Compatibility thin wrapper exposing a KattisClient instance.
2
+
3
+ Refactored: main functionality moved to `kattis_cli.client.KattisClient`.
4
+ This module keeps the original functional API by delegating to a
5
+ module-level client instance so existing call-sites do not need to change.
5
6
  """
6
7
 
7
- from typing import List, Any
8
- import sys
9
- import os
10
- import re
11
- import time
12
- import json
13
- import configparser
14
- from bs4 import BeautifulSoup
15
- import requests
16
- import requests.exceptions
17
- import requests.cookies
18
- from rich.console import Console
19
- from rich.align import Align
20
- from rich.live import Live
21
- from rich.prompt import Confirm
22
-
23
- from kattis_cli.utils import languages
24
- from kattis_cli.utils import config
25
- from kattis_cli import ui
26
-
27
- _HEADERS = {'User-Agent': 'kattis-cli-submit'}
28
-
29
- _RUNNING_STATUS = 5
30
- _COMPILE_ERROR_STATUS = 8
31
- _ACCEPTED_STATUS = 16
32
- _DEBUG = False
33
-
34
- _STATUS_MAP = {
35
- 0: 'New', # <invalid value>
36
- 1: 'New',
37
- 2: 'Waiting for compile',
38
- 3: 'Compiling',
39
- 4: 'Waiting for run',
40
- _RUNNING_STATUS: 'Running',
41
- 6: 'Judge Error',
42
- 7: 'Submission Error',
43
- _COMPILE_ERROR_STATUS: 'Compile Error',
44
- 9: 'Run Time Error',
45
- 10: 'Memory Limit Exceeded',
46
- 11: 'Output Limit Exceeded',
47
- 12: 'Time Limit Exceeded',
48
- 13: 'Illegal Function',
49
- 14: 'Wrong Answer',
50
- # 15: '<invalid value>',
51
- _ACCEPTED_STATUS: 'Accepted',
52
- }
53
-
54
-
55
- def get_url(cfg: configparser.ConfigParser, option: str, default: str) -> str:
56
- """Get a URL from the config file
57
-
58
- Args:
59
- cfg (configparser.ConfigParser): _description_
60
- option (str): option name
61
- default (str): default value
62
-
63
- Returns:
64
- str: _description_
65
- """
66
- if cfg.has_option('kattis', option):
67
- return cfg.get('kattis', option)
68
- else:
69
- return f"https://{cfg.get('kattis', 'hostname')}/{default}"
8
+ from typing import Any, List
9
+ from kattis_cli.client import KattisClient
10
+
11
+ # Single client used by module-level functions for backward compatibility
12
+ _client = KattisClient()
70
13
 
71
14
 
72
15
  def login(
73
16
  login_url: str,
74
17
  username: str,
75
18
  password: str = '',
76
- token: str = '') -> requests.Response:
77
- """Log in to Kattis.
19
+ token: str = '') -> Any:
20
+ """Convenience wrapper that delegates to the module-level client.
78
21
 
79
- At least one of password or token needs to be provided.
22
+ Kept for backward compatibility with the original procedural API.
23
+ """
80
24
 
81
- Args:
82
- login_url (str): URL to login page
83
- username (str): username
84
- password (str, optional): password. Defaults to ''
85
- token (str, optional): token. Defaults to ''
25
+ return _client.login(login_url, username, password, token)
86
26
 
87
- Returns a requests.Response with cookies needed to be able to submit
88
- """
89
- login_args = {'user': username, 'script': 'true'}
90
- if password:
91
- login_args['password'] = password
92
- if token:
93
- login_args['token'] = token
94
-
95
- return requests.post(
96
- login_url,
97
- data=login_args,
98
- headers=_HEADERS,
99
- timeout=10)
100
-
101
-
102
- def login_from_config(cfg: configparser.ConfigParser) -> requests.Response:
103
- """Log in to Kattis using the access information in a kattisrc file
104
-
105
- Args:
106
- cfg (configparser.ConfigParser)
107
- - ConfigParser object for the kattisrc file
108
- Returns:
109
- requests (requests.Response)
110
- - Response with cookies needed to be able to submit
111
- """
112
- username = cfg.get('user', 'username')
113
- password = token = ''
114
- try:
115
- password = cfg.get('user', 'password')
116
- except configparser.NoOptionError:
117
- pass
118
- try:
119
- token = cfg.get('user', 'token')
120
- except configparser.NoOptionError:
121
- pass
122
- if not password and not token:
123
- raise config.ConfigError('''\
124
- Your .kattisrc file appears corrupted. It must provide a token (or a
125
- KATTIS password).
126
-
127
- Please download a new .kattisrc file''')
128
-
129
- loginurl = get_url(cfg, 'loginurl', 'login')
130
- return login(loginurl, username, password, token)
131
-
132
-
133
- def submit(
134
- submit_url: str,
135
- cookies: requests.cookies.RequestsCookieJar,
136
- problem: str,
137
- language: str,
138
- files: List[str],
139
- mainclass: str,
140
- tag: str) -> requests.Response:
141
- """Make a submission.
142
-
143
- The url_opener argument is an OpenerDirector object to use (as
144
- returned by the login() function)
145
-
146
- Returns the requests.Result from the submission
147
- """
148
- # mainfile = utility.guess_mainfile(language, files, problem)
149
- # mainclass = mainfile
150
-
151
- data = {'submit': 'true',
152
- 'submit_ctr': 2,
153
- 'language': language,
154
- 'mainclass': mainclass,
155
- 'problem': problem,
156
- 'tag': tag,
157
- 'script': 'true'}
158
-
159
- sub_files = []
160
- for f in files:
161
- with open(f, 'rb') as sub_file:
162
- sub_files.append(('sub_file[]',
163
- (os.path.basename(f),
164
- sub_file.read(),
165
- 'application/octet-stream')))
166
-
167
- return requests.post(
27
+
28
+ def login_from_config(cfg: Any) -> Any:
29
+ """Delegate login using a parsed .kattisrc ConfigParser."""
30
+
31
+ return _client.login_from_config(cfg)
32
+
33
+
34
+ def submit(submit_url: str, cookies: Any, problem: str,
35
+ language: str, files: List[str], mainclass: str, tag: str) -> Any:
36
+ """Delegate a submission to the configured KattisClient instance."""
37
+
38
+ return _client.submit(
168
39
  submit_url,
169
- data=data,
170
- files=sub_files,
171
- cookies=cookies,
172
- headers=_HEADERS, timeout=10)
173
-
174
-
175
- def confirm_or_die(problem: str, language: str,
176
- files: List[str], mainclass: str,
177
- tag: str) -> None:
178
- """Confirm submission"""
179
- console = Console()
180
- console.clear()
181
- console.print('Problem:', problem)
182
- console.print('Language:', language)
183
- console.print('Files:', ', '.join(files))
184
- if mainclass:
185
- if language in languages.GUESS_MAINFILE:
186
- console.print('Main file:', mainclass)
187
- else:
188
- console.print('Mainclass:', mainclass)
189
- if tag:
190
- console.print('Tag:', tag)
191
- if not Confirm.ask('Submit to Kattis', default=True):
192
- console.print('Cancelling...')
193
- sys.exit(1)
194
-
195
-
196
- def get_submission_url(submit_response: str,
197
- cfg: configparser.ConfigParser
198
- ) -> str:
199
- """Get the URL of the submission from the HTML response
200
- """
201
- m = re.search(r'Submission ID: (\d+)', submit_response)
202
- if m:
203
- submissions_url = get_url(cfg, 'submissionsurl', 'submissions')
204
- submission_id = m.group(1)
205
- return f'{submissions_url}/{submission_id}'
206
- else:
207
- raise config.ConfigError('Could not find submission ID in response')
40
+ cookies,
41
+ problem,
42
+ language,
43
+ files,
44
+ mainclass,
45
+ tag)
208
46
 
209
47
 
210
- def get_submission_status(
211
- submission_url: str,
212
- cookies: requests.cookies.RequestsCookieJar
213
- ) -> Any:
214
- """Get judge status for a submission"""
215
- reply = requests.get(
216
- submission_url + '?json',
217
- cookies=cookies,
218
- headers=_HEADERS, timeout=10)
219
- return reply.json()
48
+ def get_submission_status(submission_url: str, cookies: Any) -> Any:
49
+ """Return parsed JSON status for a submission by delegating to client."""
50
+
51
+ return _client.get_submission_status(submission_url, cookies)
220
52
 
221
- # flake8: noqa: C901
53
+
54
+ def get_submission_url(submit_response: str, cfg: Any) -> str:
55
+ """Extract the submission URL from a submission response string."""
56
+
57
+ return _client.get_submission_url(submit_response, cfg)
222
58
 
223
59
 
224
60
  def parse_row_html(html: str) -> Any:
225
- """Parse row_html value from Kattis JASON response.
61
+ """Parse a submission HTML row and return a structured tuple."""
226
62
 
227
- Args:
228
- html (str): html string
63
+ return _client.parse_row_html(html)
229
64
 
230
- Returns:
231
- Tuple: runtime, status, language, total_cases, done_cases
232
- """
233
- # print(html)
234
- runtime = '❓ s'
235
- status = '❓'
236
- language = '❓'
237
- test_status = '❓/❓'
238
- soup = BeautifulSoup(html, 'html.parser')
239
- tr_submission: Any = soup.find("tr", {"data-submission-id": True})
240
- # print(tr_submission)
241
- if tr_submission:
242
- td_cputime = tr_submission.findChild("td", {"data-type": "cpu"})
243
- if td_cputime:
244
- runtime = td_cputime.text.strip().replace('&nbsp;', ' ')
245
- if not runtime:
246
- runtime = '❓ s'
247
- div_status = tr_submission.findChild(
248
- "div", {"class": "status"}, recursive=True)
249
- if div_status:
250
- status = div_status.text.strip()
251
- else:
252
- status = '❓'
253
- td_lang = tr_submission.findChild(
254
- "td", {"data-type": "lang"}, recursive=False)
255
- if td_lang:
256
- language = td_lang.text.strip()
257
- else:
258
- language = '❓'
259
- td_test_cases = tr_submission.findChild(
260
- "td", {"data-type": "testcases"})
261
- if td_test_cases:
262
- test_status = td_test_cases.text.strip()
263
-
264
- i_tag = soup.findAll("i", {"class": True})
265
- test_result = []
266
- for num, i in enumerate(i_tag):
267
- if 'title' not in i.attrs:
268
- continue
269
- title = i['title'].split(':')
270
- # print(title[-1])
271
- if 'Accepted' in title[-1]: # Accepted
272
- test_result.append(f'{num}✅')
273
- elif 'not checked' in title[-1]: # not checked
274
- test_result.append(f'{num}❓')
275
- else:
276
- test_result.append(f'{num}❌')
277
- return runtime, status, language, test_status, test_result
278
-
279
-
280
- def show_kattis_judgement(problemid: str, submission_url: str,
281
- cfg: configparser.ConfigParser) -> None:
282
- """Show judgement from Kattis.
283
- """
284
- config_data = ui.show_problem_metadata(problemid)
285
- config_data['submissions'] += 1
286
- console = Console()
287
- login_reply = 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
- counter = 1
294
- while True:
295
- time.sleep(0.1)
296
- result = get_submission_status(submission_url,
297
- login_reply.cookies)
298
- if _DEBUG:
299
- with open(f'{counter}.json', 'w', encoding='utf-8') as f:
300
- json.dump(result, f, indent=4)
301
- rt, status, lang, t_status, t_results = parse_row_html(
302
- result['row_html'])
303
- # console.print(f'{runtime=} {status=} {language=}')
304
- status_id = int(result['status_id'])
305
- # cases_done = int(result['testcase_index'])
306
- if status_id > 4 and status_id < 16:
307
- judgement = f'[bold red] {status}[/]'
308
- else:
309
- judgement = f'[bold yellow] {status}[/]'
310
- if status_id == 12:
311
- runtime = f'[bold red] {rt}[/]'
312
- else:
313
- runtime = f'[bold yellow] {rt}[/]'
314
-
315
- text = title + f'\n[bold blue]JUDGEMENT:[/] {judgement}'
316
- text += f'\t[bold blue]LANGUAGE:[/] [bold yellow]{lang}[/]'
317
- text += f'\t[bold blue]RUNTIME:[/] {runtime}'
318
- text += f'\t[bold deep_pink3][link={submission_url}]'
319
- text += 'VIEW DETAILS ON KATTIS[/link][/]\n\n'
320
- text += f'[bold blue]TESTCASES:[/] [bold green]{t_status}[/]\n\n'
321
- if status_id < 5:
322
- test_cases = '🤞🏻🤞🏻🤞🏻🤞🏻🤞🏻 [bold yellow]\
323
- WAITING...[/] 🤞🏻🤞🏻🤞🏻🤞🏻🤞🏻‍'
324
- else:
325
- test_cases = ' '.join(t_results)
326
- test_cases += '\n'
327
- text += test_cases
328
- kattis_live.update(Align.center(text))
329
- if status_id > 5:
330
- kattis_live.stop()
331
- break
332
- if status_id == _ACCEPTED_STATUS:
333
- verdict = '👍🎆🔥🎈🎈 [bold yellow]YAY!! \
334
- KEEP GOING...[/] 🎈🎈👍🎆🔥'
335
- console.print()
336
- config_data['accepted'] += 1
337
- else:
338
- verdict = '💪🧐💪 [bold green]SORRY![/] 🧐💪🧐'
339
- console.print(Align.center(verdict))
340
- config.update_problem_metadata(problemid, config_data)
341
- config_data = ui.show_problem_metadata(problemid)
342
-
343
-
344
- def get_login_reply(cfg: configparser.ConfigParser) -> requests.Response:
345
- """Log in to Kattis.
346
- """
347
- console = Console()
348
-
349
- try:
350
- login_reply = login_from_config(cfg)
351
- except config.ConfigError as exc:
352
- console.print(exc)
353
- sys.exit(1)
354
- except requests.exceptions.RequestException as err:
355
- console.print('Login connection failed:', err)
356
- sys.exit(1)
357
-
358
- if not login_reply.status_code == 200:
359
- console.print('Login failed.')
360
- if login_reply.status_code == 403:
361
- console.print('Incorrect username or password/token (403)')
362
- elif login_reply.status_code == 404:
363
- console.print('Incorrect login URL (404)')
364
- else:
365
- console.print('Status code:', login_reply.status_code)
366
- sys.exit(1)
367
- return login_reply
368
-
369
-
370
- def submit_solution(files: List[str], problemid: str,
371
- language: str, mainclass: str,
372
- tag: str, force: bool) -> None:
373
- """Submit a solution to Kattis.
374
-
375
- Args:
376
- files (Tuple[str]): Tuple of files to submit
377
- problem (str, optional): problemid. Defaults to ''.
378
- language (str, optional): language. Defaults to ''.
379
- mainclass (str, optional): main class/file. Defaults to ''.
380
- tag (str, optional): Tag. Defaults to ''.
381
- force (bool, optional): Force bumission without confirmation.
65
+
66
+ def show_kattis_judgement(
67
+ problemid: str,
68
+ submission_url: str,
69
+ cfg: Any) -> None:
70
+ """Display live judgement output for a submission.
71
+
72
+ This is a thin wrapper around the client's method.
382
73
  """
383
- console = Console()
384
- try:
385
- cfg = config.get_kattisrc()
386
- except config.ConfigError as exc:
387
- console.print(exc)
388
- sys.exit(1)
389
- login_reply = get_login_reply(cfg)
390
- if not files:
391
- console.print('No files specified')
392
- sys.exit(1)
393
-
394
- files = sorted(list(set(files)))
395
-
396
- submit_url = get_url(cfg, 'submissionurl', 'submit')
397
-
398
- if not force:
399
- confirm_or_die(problemid, language, files, mainclass, tag)
400
-
401
- try:
402
- result = submit(submit_url,
403
- login_reply.cookies,
404
- problemid,
405
- language,
406
- files,
407
- mainclass,
408
- tag)
409
- except requests.exceptions.RequestException as err:
410
- console.print('Submit connection failed:', err, style='bold red')
411
- sys.exit(1)
412
-
413
- if result.status_code != 200:
414
- console.print('Submission failed.', style='bold red')
415
- if result.status_code == 403:
416
- console.print('Access denied (403)', style='bold red')
417
- elif result.status_code == 404:
418
- console.print('Incorrect submit URL (404)')
419
- else:
420
- console.print('Status code:', result.status_code, style='bold red')
421
- sys.exit(1)
422
-
423
- plain_result = result.content.decode('utf-8').replace('<br />', '\n')
424
- console.print(f'[bold blue]{plain_result}[/]')
425
-
426
- submission_url = None
427
- try:
428
- submission_url = get_submission_url(plain_result, cfg)
429
- except configparser.NoOptionError:
430
- pass
431
-
432
- if submission_url:
433
- show_kattis_judgement(problemid, submission_url, cfg)
74
+
75
+ return _client.show_kattis_judgement(problemid, submission_url, cfg)
76
+
77
+
78
+ def get_login_reply(cfg: Any) -> Any:
79
+ """Obtain a logged-in requests.Response using config credentials."""
80
+
81
+ return _client.get_login_reply(cfg)
82
+
83
+
84
+ def submit_solution(files: List[str], problemid: str, language: str,
85
+ mainclass: str, tag: str, force: bool) -> None:
86
+ """High-level helper to submit solution files for a problem id."""
87
+
88
+ return _client.submit_solution(
89
+ files, problemid, language, mainclass, tag, force)