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/kattis_setup.py
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
"""Setup
|
|
1
|
+
"""Setup helpers for Kattis CLI.
|
|
2
|
+
|
|
3
|
+
This module now exposes a :class:`SetupManager` which accepts a
|
|
4
|
+
``KattisClient`` in order to perform login flows. A module-level
|
|
5
|
+
``setup`` function delegates to a default manager to preserve the
|
|
6
|
+
original API used by the CLI.
|
|
2
7
|
"""
|
|
3
8
|
|
|
4
9
|
from pathlib import Path
|
|
5
10
|
import requests
|
|
6
11
|
from rich.console import Console
|
|
7
12
|
from rich.prompt import Prompt, Confirm
|
|
8
|
-
from
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from .client import KattisClient
|
|
9
15
|
from .utils import config
|
|
10
16
|
|
|
11
|
-
|
|
12
17
|
_LOGIN_URL = 'https://open.kattis.com/login'
|
|
13
18
|
_KATTISRCURL = "https://open.kattis.com/download/kattisrc"
|
|
14
19
|
|
|
15
|
-
#
|
|
16
|
-
_HEADERS = {
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
# headers used to fetch kattisrc during interactive setup
|
|
21
|
+
_HEADERS = {
|
|
22
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
|
23
|
+
'(KHTML, like Gecko) '
|
|
24
|
+
'Ubuntu Chromium/83.0.4103.97 Chrome/83.0.4103.97 Safari/537.36'}
|
|
19
25
|
_HEADERS['Referer'] = _LOGIN_URL
|
|
20
26
|
_HEADERS['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
21
27
|
_HEADERS['Origin'] = 'https://open.kattis.com'
|
|
@@ -23,96 +29,127 @@ _HEADERS['Host'] = 'open.kattis.com'
|
|
|
23
29
|
_HEADERS['Connection'] = 'keep-alive'
|
|
24
30
|
_HEADERS['Accept-Language'] = 'en-US,en;q=0.9'
|
|
25
31
|
_HEADERS['Accept-Encoding'] = 'gzip, deflate, br'
|
|
26
|
-
_HEADERS['Accept'] =
|
|
27
|
-
application/xml;q=0.9,
|
|
32
|
+
_HEADERS['Accept'] = (
|
|
33
|
+
'text/html,application/xhtml+xml,application/xml;q=0.9,'
|
|
34
|
+
'image/webp,*/*;q=0.8'
|
|
35
|
+
)
|
|
28
36
|
|
|
29
37
|
_KATTISRC = Path.home().joinpath(".kattisrc")
|
|
30
38
|
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
"""
|
|
40
|
+
class SetupManager:
|
|
41
|
+
"""Interactive setup manager for installing .kattisrc.
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
The manager accepts a `KattisClient` instance so the underlying
|
|
44
|
+
authentication logic can be injected (useful for testing).
|
|
37
45
|
"""
|
|
38
|
-
try:
|
|
39
|
-
cfg = config.get_kattisrc()
|
|
40
|
-
response = kattis.login_from_config(cfg)
|
|
41
|
-
if response.status_code != 200:
|
|
42
|
-
return False
|
|
43
|
-
except config.ConfigError:
|
|
44
|
-
return False
|
|
45
|
-
return True
|
|
46
46
|
|
|
47
|
+
def __init__(self, client: Optional[KattisClient] = None) -> None:
|
|
48
|
+
self.client = client or KattisClient()
|
|
47
49
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
def check_kattisrc(self) -> bool:
|
|
51
|
+
"""Check if kattisrc file exists and is valid.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
bool: True if kattisrc is valid, False otherwise.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
cfg = config.get_kattisrc()
|
|
58
|
+
response = self.client.login_from_config(cfg)
|
|
59
|
+
if response.status_code != 200:
|
|
60
|
+
return False
|
|
61
|
+
except config.ConfigError:
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
def setup(self) -> None:
|
|
66
|
+
"""Run the interactive setup for Kattis CLI.
|
|
67
|
+
"""
|
|
68
|
+
console = Console()
|
|
69
|
+
console.print(":cat: Welcome to Kattis CLI! :cat:", style="bold blue")
|
|
70
|
+
if self.check_kattisrc():
|
|
71
|
+
console.print(
|
|
72
|
+
":rocket: [bold blue]kattisrc already exists with valid "
|
|
73
|
+
"token.[/]"
|
|
74
|
+
)
|
|
75
|
+
console.print(
|
|
76
|
+
":rocket: [bold green]You are ready to use Kattis CLI.[/]"
|
|
65
77
|
)
|
|
78
|
+
return
|
|
79
|
+
console.print("You need your Kattis credentials for this setup.")
|
|
80
|
+
if Confirm.ask("Do you have an account on Kattis?", default=True):
|
|
66
81
|
while True:
|
|
67
|
-
|
|
68
|
-
"Please enter your
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
username = Prompt.ask(
|
|
83
|
+
"Please enter your username or email", password=False)
|
|
84
|
+
while True:
|
|
85
|
+
password = Prompt.ask(
|
|
86
|
+
"Please enter your password", password=True)
|
|
87
|
+
if Confirm.ask(
|
|
88
|
+
"Want to see your password? ",
|
|
89
|
+
default=False):
|
|
90
|
+
console.print(f"You entered: {password}")
|
|
91
|
+
if Confirm.ask("Is this correct? ", default=True):
|
|
92
|
+
console.print(":rocket: Logging in...")
|
|
93
|
+
break
|
|
94
|
+
else:
|
|
74
95
|
console.print(":rocket: Logging in...")
|
|
75
96
|
break
|
|
97
|
+
|
|
98
|
+
response = self.client.login(_LOGIN_URL, username, password)
|
|
99
|
+
if response.status_code == 200:
|
|
100
|
+
console.print(":rocket: Login successful!")
|
|
101
|
+
res = requests.get(
|
|
102
|
+
_KATTISRCURL,
|
|
103
|
+
cookies=response.cookies,
|
|
104
|
+
headers=_HEADERS,
|
|
105
|
+
timeout=10,
|
|
106
|
+
)
|
|
107
|
+
if res.status_code == 200:
|
|
108
|
+
with open(_KATTISRC, "w", encoding='utf-8') as f:
|
|
109
|
+
f.write(res.text)
|
|
110
|
+
console.print(
|
|
111
|
+
f":rocket: kattisrc downloaded and saved to "
|
|
112
|
+
f"{str(_KATTISRC)}"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
text = (
|
|
116
|
+
":loudly_crying_face:\n"
|
|
117
|
+
"kattisrc download failed. Please download "
|
|
118
|
+
"kattisrc manually from:\n"
|
|
119
|
+
"https://open.kattis.com/download/kattisrc\n"
|
|
120
|
+
"and save it to your system's home directory with "
|
|
121
|
+
"the name .kattisrc\n"
|
|
122
|
+
)
|
|
123
|
+
console.print(text)
|
|
124
|
+
break
|
|
76
125
|
else:
|
|
77
|
-
console.print(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
and save it to your system's home directory with the name .kattisrc
|
|
108
|
-
"""
|
|
109
|
-
console.print(text)
|
|
110
|
-
break
|
|
111
|
-
else:
|
|
112
|
-
console.print(""":loudly_crying_face:
|
|
113
|
-
Login failed. Please try again.""")
|
|
114
|
-
else:
|
|
115
|
-
console.print("Please register your account at: ")
|
|
116
|
-
console.print("""[link="https://open.kattis.com/register"]
|
|
117
|
-
https://open.kattis.com/register[/link]""")
|
|
118
|
-
console.print("and try again.")
|
|
126
|
+
console.print(
|
|
127
|
+
":loudly_crying_face:\n"
|
|
128
|
+
"Login failed. Please try again."
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
console.print("Please register your account at: ")
|
|
132
|
+
console.print(
|
|
133
|
+
"[link=\"https://open.kattis.com/register\"]"
|
|
134
|
+
"https://open.kattis.com/register[/link]"
|
|
135
|
+
)
|
|
136
|
+
console.print("and try again.")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Default manager for compatibility
|
|
140
|
+
_manager = SetupManager()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def check_kattisrc() -> bool:
|
|
144
|
+
"""Check if kattisrc file exists and is valid.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
bool: True if kattisrc is valid, False otherwise.
|
|
148
|
+
"""
|
|
149
|
+
return _manager.check_kattisrc()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def setup() -> None:
|
|
153
|
+
"""Wrapper for Katti-cli setup
|
|
154
|
+
"""
|
|
155
|
+
return _manager.setup()
|
kattis_cli/main.py
CHANGED
|
@@ -7,7 +7,7 @@ build.sh script copies the contents of this file to main.py.
|
|
|
7
7
|
Change the __version__ to match in pyproject.toml
|
|
8
8
|
Has to be higher than the pypi version.
|
|
9
9
|
"""
|
|
10
|
-
__version__ = '1.0
|
|
10
|
+
__version__ = '1.1.0'
|
|
11
11
|
|
|
12
12
|
from math import inf
|
|
13
13
|
from typing import Tuple
|
|
@@ -17,7 +17,7 @@ import requests
|
|
|
17
17
|
from trogon import tui
|
|
18
18
|
import kattis_cli.download as download
|
|
19
19
|
import kattis_cli.ui as ui
|
|
20
|
-
import kattis_cli.
|
|
20
|
+
import kattis_cli.solution_tester as solution_tester
|
|
21
21
|
import kattis_cli.kattis as kattis
|
|
22
22
|
import kattis_cli.utils.languages as languages
|
|
23
23
|
import kattis_cli.kattis_setup as kattis_setup
|
|
@@ -84,7 +84,7 @@ def test(
|
|
|
84
84
|
mainclass = languages.guess_mainfile(
|
|
85
85
|
language, _files, problemid, lang_config)
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
solution_tester.test_samples(
|
|
88
88
|
problemid,
|
|
89
89
|
loc_language,
|
|
90
90
|
mainclass,
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Solution tester module for Kattis.
|
|
2
|
+
|
|
3
|
+
Provides a SolutionTester class and a module-level `test_samples`
|
|
4
|
+
delegator for backward compatibility with the previous procedural API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, List, Dict, Optional
|
|
8
|
+
from math import inf
|
|
9
|
+
import glob
|
|
10
|
+
import time
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.live import Live
|
|
16
|
+
from rich.align import Align
|
|
17
|
+
from rich import box
|
|
18
|
+
from rich.prompt import Confirm
|
|
19
|
+
from rich.markup import escape
|
|
20
|
+
|
|
21
|
+
from kattis_cli import kattis
|
|
22
|
+
from kattis_cli.utils import languages, run_program, utility
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SolutionTester:
|
|
26
|
+
"""Encapsulates testing of solutions using sample data.
|
|
27
|
+
|
|
28
|
+
Accepts optional collaborators to make testing easier to mock in
|
|
29
|
+
unit tests.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
client: Optional[Any] = None,
|
|
35
|
+
download_manager: Optional[Any] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Create a SolutionTester.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
client: Optional client or module providing submit_solution.
|
|
41
|
+
download_manager: Optional download manager instance.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
self.client = client or kattis
|
|
45
|
+
self.download_manager = download_manager
|
|
46
|
+
|
|
47
|
+
def test_samples(
|
|
48
|
+
self,
|
|
49
|
+
problemid: str,
|
|
50
|
+
loc_language: str,
|
|
51
|
+
mainclass: str,
|
|
52
|
+
problem_root_folder: str,
|
|
53
|
+
files: List[str],
|
|
54
|
+
lang_config: Dict[Any, Any],
|
|
55
|
+
accuracy: float = inf
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Run the sample tests for a solution.
|
|
58
|
+
|
|
59
|
+
This mirrors the previous procedural `test_samples` function but
|
|
60
|
+
is encapsulated on a class to allow dependency injection for
|
|
61
|
+
easier testing.
|
|
62
|
+
"""
|
|
63
|
+
console = Console()
|
|
64
|
+
|
|
65
|
+
table = Table(show_header=True,
|
|
66
|
+
header_style="bold blue",
|
|
67
|
+
show_lines=True,
|
|
68
|
+
show_footer=False)
|
|
69
|
+
table.box = box.SQUARE
|
|
70
|
+
table_centered = Align.center(table)
|
|
71
|
+
|
|
72
|
+
sep = os.path.sep
|
|
73
|
+
in_files = glob.glob(f"{problem_root_folder}{sep}data{sep}*.in")
|
|
74
|
+
if not in_files:
|
|
75
|
+
data_path = f"{problem_root_folder}{sep}data"
|
|
76
|
+
console.print(data_path, style="bold blue")
|
|
77
|
+
console.print("No sample input files found!", style="bold red")
|
|
78
|
+
exit(1)
|
|
79
|
+
in_files.sort()
|
|
80
|
+
if lang_config['compile']:
|
|
81
|
+
ex_code, ans, error = run_program.compile_program(
|
|
82
|
+
lang_config,
|
|
83
|
+
files,
|
|
84
|
+
)
|
|
85
|
+
if ex_code != 0: # compilation error; exit code
|
|
86
|
+
console.print(escape(error), style='bold red')
|
|
87
|
+
exit(1)
|
|
88
|
+
console.print('Compiled successfully!', style='bold green')
|
|
89
|
+
|
|
90
|
+
count = 0
|
|
91
|
+
total = len(in_files)
|
|
92
|
+
console.clear()
|
|
93
|
+
title = f"[not italic bold blue]👷 Testing {mainclass} "
|
|
94
|
+
title += f" using {loc_language} 👷[/]"
|
|
95
|
+
table.title = title
|
|
96
|
+
with Live(table_centered, console=console,
|
|
97
|
+
screen=False, refresh_per_second=10):
|
|
98
|
+
table.add_column(
|
|
99
|
+
"Input File",
|
|
100
|
+
justify="center",
|
|
101
|
+
style="cyan",
|
|
102
|
+
no_wrap=False)
|
|
103
|
+
table.add_column(
|
|
104
|
+
"Sample Input",
|
|
105
|
+
justify="left",
|
|
106
|
+
style="cyan",
|
|
107
|
+
no_wrap=False)
|
|
108
|
+
table.add_column(
|
|
109
|
+
"Output File",
|
|
110
|
+
justify="center",
|
|
111
|
+
style="cyan",
|
|
112
|
+
no_wrap=False)
|
|
113
|
+
table.add_column(
|
|
114
|
+
"Expected Output",
|
|
115
|
+
justify="left",
|
|
116
|
+
style="cyan",
|
|
117
|
+
no_wrap=False)
|
|
118
|
+
table.add_column(
|
|
119
|
+
"Program Output",
|
|
120
|
+
justify="left",
|
|
121
|
+
style="cyan",
|
|
122
|
+
no_wrap=False)
|
|
123
|
+
table.add_column(
|
|
124
|
+
"Result",
|
|
125
|
+
justify="center",
|
|
126
|
+
style="cyan",
|
|
127
|
+
no_wrap=True)
|
|
128
|
+
|
|
129
|
+
for in_file in in_files:
|
|
130
|
+
with open(in_file, 'rb') as f:
|
|
131
|
+
input_content = f.read()
|
|
132
|
+
input_content.replace(b'\r\n', b'\n') # Windows fix
|
|
133
|
+
out_file = in_file.replace('.in', '.ans')
|
|
134
|
+
try:
|
|
135
|
+
with open(out_file, 'rb') as f:
|
|
136
|
+
expected = f.read()
|
|
137
|
+
expected.replace(b'\r\n', b'\n')
|
|
138
|
+
except FileNotFoundError:
|
|
139
|
+
try:
|
|
140
|
+
out_file = in_file.replace('.in', '.out')
|
|
141
|
+
with open(out_file, 'rb') as f:
|
|
142
|
+
expected = f.read()
|
|
143
|
+
expected.replace(b'\r\n', b'\n')
|
|
144
|
+
except FileNotFoundError:
|
|
145
|
+
expected = b"No .ans or .out file found!"
|
|
146
|
+
code, ans, error = run_program.run(
|
|
147
|
+
lang_config,
|
|
148
|
+
mainclass,
|
|
149
|
+
in_file,
|
|
150
|
+
)
|
|
151
|
+
if code != 0:
|
|
152
|
+
ans = error
|
|
153
|
+
|
|
154
|
+
if utility.check_answer(expected.decode('utf-8'),
|
|
155
|
+
ans, accuracy):
|
|
156
|
+
result = "[bold green]✅[/bold green]"
|
|
157
|
+
count += 1
|
|
158
|
+
else:
|
|
159
|
+
result = "[bold red]❌[/bold red]"
|
|
160
|
+
|
|
161
|
+
in_filename = Path(in_file).parts[-1]
|
|
162
|
+
out_filename = Path(out_file).parts[-1]
|
|
163
|
+
time.sleep(0.1)
|
|
164
|
+
table.add_row(in_filename,
|
|
165
|
+
input_content.decode('utf-8'),
|
|
166
|
+
out_filename,
|
|
167
|
+
escape(expected.decode('utf-8')),
|
|
168
|
+
escape(ans),
|
|
169
|
+
result)
|
|
170
|
+
if code != 0 and 'SyntaxError: ' in error:
|
|
171
|
+
table.columns[4].style = 'bold red'
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
data_path = f"{problem_root_folder}{sep}data"
|
|
175
|
+
console.print(data_path, style="bold blue")
|
|
176
|
+
console.print(f'Total {total} input/output sample(s) found.')
|
|
177
|
+
console.print(f"{count}/{total} tests passed.")
|
|
178
|
+
if count < total:
|
|
179
|
+
console.print("Check the output columns for differences.")
|
|
180
|
+
console.print("Keep trying!")
|
|
181
|
+
else:
|
|
182
|
+
console.print(
|
|
183
|
+
"Awesome... Time to submit it to :cat: Kattis! :cat:",
|
|
184
|
+
style="bold green",
|
|
185
|
+
)
|
|
186
|
+
if Confirm.ask("Submit to Kattis?", default=True):
|
|
187
|
+
kat_language = (
|
|
188
|
+
languages.LOCAL_TO_KATTIS.get(loc_language, '')
|
|
189
|
+
)
|
|
190
|
+
self.client.submit_solution(
|
|
191
|
+
files,
|
|
192
|
+
problemid,
|
|
193
|
+
kat_language,
|
|
194
|
+
mainclass,
|
|
195
|
+
tag="",
|
|
196
|
+
force=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Default manager for module-level compatibility
|
|
201
|
+
_tester = SolutionTester()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_samples(
|
|
205
|
+
problemid: str,
|
|
206
|
+
loc_language: str,
|
|
207
|
+
mainclass: str,
|
|
208
|
+
problem_root_folder: str,
|
|
209
|
+
files: List[str],
|
|
210
|
+
lang_config: Dict[Any, Any],
|
|
211
|
+
accuracy: float = inf
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Module-level wrapper delegating to the :class:`SolutionTester`.
|
|
214
|
+
|
|
215
|
+
Keeps the original procedural API for callers that import
|
|
216
|
+
`test_samples` directly from the module.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
return _tester.test_samples(problemid, loc_language, mainclass,
|
|
220
|
+
problem_root_folder, files, lang_config,
|
|
221
|
+
accuracy)
|