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.
@@ -1,21 +1,27 @@
1
- """Setup file for Kattis.
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 . import kattis
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
- # _HEADERS = {'User-Agent': 'kattis-cli'}
16
- _HEADERS = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
17
- (KHTML, like Gecko) \
18
- Ubuntu Chromium/83.0.4103.97 Chrome/83.0.4103.97 Safari/537.36'}
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'] = 'text/html,application/xhtml+xml,\
27
- application/xml;q=0.9,image/webp,*/*;q=0.8'
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
- def check_kattisrc() -> bool:
33
- """Check if kattisrc exists.
40
+ class SetupManager:
41
+ """Interactive setup manager for installing .kattisrc.
34
42
 
35
- Returns:
36
- bool: True if kattisrc exists, False otherwise.
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 setup() -> None:
49
- """Setup Kattis CLI.
50
- """
51
- console = Console()
52
- console.print(":cat: Welcome to Kattis CLI! :cat:", style="bold blue")
53
- if check_kattisrc():
54
- console.print(
55
- ":rocket: [bold blue]kattisrc already exists with valid token.[/]")
56
- console.print(
57
- ":rocket: [bold green]You are ready to use Kattis CLI.[/]")
58
- return
59
- console.print("You need your Kattis credentials for this setup.")
60
- if Confirm.ask("Do you have an account on Kattis?", default=True):
61
- while True:
62
- username = Prompt.ask(
63
- "Please enter your username or email",
64
- password=False,
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
- password = Prompt.ask(
68
- "Please enter your password",
69
- password=True,
70
- )
71
- if Confirm.ask("Want to see your password? ", default=False):
72
- console.print(f"You entered: {password}")
73
- if Confirm.ask("Is this correct? ", default=True):
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(":rocket: Logging in...")
78
- break
79
-
80
- response = kattis.login(_LOGIN_URL, username, password)
81
- # console.print("code=", response.status_code)
82
- # console.print("text=", response.text)
83
- if response.status_code == 200:
84
- console.print(":rocket: Login successful!")
85
- # download kattisrc
86
- # cookies = response.cookies
87
- # console.print(cookies)
88
- # cookies = response.cookies.RequestsCookieJar
89
- res = requests.get(
90
- _KATTISRCURL,
91
- cookies=response.cookies,
92
- headers=_HEADERS,
93
- timeout=10,
94
- )
95
- # console.print(res.status_code)
96
- # console.print(res.text)
97
- if res.status_code == 200:
98
- with open(_KATTISRC, "w", encoding='utf-8') as f:
99
- f.write(res.text)
100
- console.print(f""":rocket: kattisrc
101
- downloaded and saved to {str(_KATTISRC)}""")
102
- else:
103
- text = """:loudly_crying_face:
104
- kattisrc download failed. Please download kattisrc manually from:
105
- [link=https://open.kattis.com/download/kattisrc]
106
- https://open.kattis.com/download/kattisrc[/link]
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.7'
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.test_solution as test_solution
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
- test_solution.test_samples(
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)