AtCoderStudyBooster 0.3__py3-none-any.whl → 0.3.2__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.
atcdr/open.py CHANGED
@@ -3,41 +3,46 @@ from rich.panel import Panel
3
3
  from rich.console import Console
4
4
 
5
5
  from atcdr.util.filetype import Lang
6
- from atcdr.util.execute import execute_files
7
- from atcdr.util.problem import find_link_from_html
6
+ from atcdr.util.fileops import add_file_selector
7
+ import rich_click as click
8
+ from atcdr.util.parse import ProblemHTML
8
9
 
9
10
 
10
11
  def open_html(file: str) -> None:
11
- console = Console()
12
- try:
13
- with open(file, 'r') as f:
14
- html_content = f.read()
15
- except FileNotFoundError:
16
- console.print(
17
- Panel(
18
- f"{file}' [red]が見つかりません[/]",
19
- border_style='red',
20
- )
21
- )
22
- return
12
+ console = Console()
13
+ try:
14
+ with open(file, 'r') as f:
15
+ html_content = f.read()
16
+ except FileNotFoundError:
17
+ console.print(
18
+ Panel(
19
+ f"{file}' [red]が見つかりません[/]",
20
+ border_style='red',
21
+ )
22
+ )
23
+ return
23
24
 
24
- url = find_link_from_html(html_content)
25
- if url:
26
- webbrowser.open_new_tab(url)
27
- console.print(
28
- Panel(
29
- f'[green]URLを開きました[/] {url}',
30
- border_style='green',
31
- )
32
- )
33
- else:
34
- console.print(
35
- Panel(
36
- f'{file} [yellow]にURLが見つかりませんでした[/]',
37
- border_style='yellow',
38
- )
39
- )
25
+ url = ProblemHTML(html_content).link
26
+ if url:
27
+ webbrowser.open_new_tab(url)
28
+ console.print(
29
+ Panel(
30
+ f'[green]URLを開きました[/] {url}',
31
+ border_style='green',
32
+ )
33
+ )
34
+ else:
35
+ console.print(
36
+ Panel(
37
+ f'{file} [yellow]にURLが見つかりませんでした[/]',
38
+ border_style='yellow',
39
+ )
40
+ )
40
41
 
41
42
 
42
- def open_files(*args: str) -> None:
43
- execute_files(*args, func=open_html, target_filetypes=[Lang.HTML])
43
+ @click.command(short_help='HTMLファイルを開く')
44
+ @add_file_selector('files', filetypes=[Lang.HTML])
45
+ def open_files(files):
46
+ """指定したHTMLファイルをブラウザで開きます。"""
47
+ for path in files:
48
+ open_html(path)
atcdr/submit.py ADDED
@@ -0,0 +1,302 @@
1
+ import os
2
+ import re
3
+ import time
4
+ from typing import Dict, List, NamedTuple, Optional
5
+
6
+ import questionary as q
7
+ import requests
8
+ import rich_click as click
9
+ import webview
10
+ from bs4 import BeautifulSoup as bs
11
+ from rich import print
12
+ from rich.live import Live
13
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
14
+ from rich.status import Status
15
+
16
+ from atcdr.login import login
17
+ from atcdr.test import (
18
+ ResultStatus,
19
+ TestInformation,
20
+ TestRunner,
21
+ create_renderable_test_info,
22
+ )
23
+ from atcdr.util.fileops import add_file_selector
24
+ from atcdr.util.filetype import (
25
+ COMPILED_LANGUAGES,
26
+ INTERPRETED_LANGUAGES,
27
+ Lang,
28
+ detect_language,
29
+ lang2str,
30
+ str2lang,
31
+ )
32
+ from atcdr.util.parse import ProblemHTML, get_submission_id
33
+ from atcdr.util.session import load_session, validate_session
34
+
35
+
36
+ class LanguageOption(NamedTuple):
37
+ id: int
38
+ display_name: str
39
+ lang: Lang
40
+
41
+
42
+ def convert_options_to_langs(options: Dict[str, int]) -> List[LanguageOption]:
43
+ lang_options = []
44
+ for display_name, id_value in options.items():
45
+ lang_name = display_name.split()[
46
+ 0
47
+ ].lower() # 例えば,C++ 23 (Clang 16.0.6)から「c++」を取り出す
48
+ try:
49
+ lang = str2lang(lang_name)
50
+ except KeyError:
51
+ continue
52
+ lang_options.append(
53
+ LanguageOption(id=id_value, display_name=display_name, lang=lang)
54
+ )
55
+
56
+ return lang_options
57
+
58
+
59
+ def choose_langid_interactively(lang_dict: dict, lang: Lang) -> int:
60
+ options = convert_options_to_langs(lang_dict)
61
+ options = [*filter(lambda option: option.lang == lang, options)]
62
+
63
+ langid = q.select(
64
+ message=f'以下の一覧から{lang2str(lang)}の実装/コンパイラーを選択してください',
65
+ qmark='',
66
+ pointer='❯❯❯',
67
+ choices=[
68
+ q.Choice(title=f'{option.display_name}', value=option.id)
69
+ for option in options
70
+ ],
71
+ instruction='\n 十字キーで移動,[enter]で実行',
72
+ style=q.Style(
73
+ [
74
+ ('question', 'fg:#2196F3 bold'),
75
+ ('answer', 'fg:#FFB300 bold'),
76
+ ('pointer', 'fg:#FFB300 bold'),
77
+ ('highlighted', 'fg:#FFB300 bold'),
78
+ ('selected', 'fg:#FFB300 bold'),
79
+ ]
80
+ ),
81
+ ).ask()
82
+
83
+ return langid
84
+
85
+
86
+ def post_source(source_path: str, url: str, session: requests.Session) -> Optional[str]:
87
+ with open(source_path, 'r') as f:
88
+ source = f.read()
89
+
90
+ problem_html = session.get(url).text
91
+ problem = ProblemHTML(problem_html)
92
+ lang_dict = problem.form.get_languages_options()
93
+ lang = detect_language(source_path)
94
+ langid = choose_langid_interactively(lang_dict, lang)
95
+
96
+ api = type('API', (), {'html': None, 'url': None})()
97
+ window = webview.create_window(
98
+ 'AtCoder Submit', url, js_api=api, width=800, height=600, hidden=False
99
+ )
100
+
101
+ def on_loaded():
102
+ current = window.get_current_url()
103
+
104
+ if current != url:
105
+ dom = window.evaluate_js('document.documentElement.outerHTML')
106
+ api.html = dom
107
+ api.url = current
108
+ window.destroy()
109
+ else:
110
+ safe_src = source.replace('\\', '\\\\').replace('`', '\\`')
111
+ inject_js = f"""
112
+ (function() {{
113
+ // Populate ACE editor
114
+ var ed = ace.edit('editor');
115
+ ed.setValue(`{safe_src}`, -1);
116
+ // Sync to hidden textarea
117
+ document.getElementById('plain-textarea').value = ed.getValue();
118
+ // Select language
119
+ var sel = document.querySelector('select[name=\"data.LanguageId\"]');
120
+ sel.value = '{langid}'; sel.dispatchEvent(new Event('change', {{ bubbles: true }}));
121
+
122
+ // CloudFlare Turnstile handling
123
+ var cf = document.querySelector('input[name=\"cf-turnstile-response\"]');
124
+ if (cf) {{
125
+ // observe token and submit when ready
126
+ new MutationObserver(function() {{
127
+ if (cf.value) {{ document.getElementById('submit').click(); }}
128
+ }}).observe(cf, {{ attributes: true }});
129
+ }} else {{
130
+ // no Turnstile present, submit immediately
131
+ document.getElementById('submit').click();
132
+ }}
133
+ }})();
134
+ """
135
+ window.evaluate_js(inject_js)
136
+
137
+ window.events.loaded += on_loaded
138
+
139
+ with Status('キャプチャー認証を解決してください', spinner='circleHalves'):
140
+ webview.start(private_mode=False)
141
+
142
+ if 'submit' in api.url:
143
+ print('[red][-][/red] 提出に失敗しました')
144
+ return None
145
+ elif 'submissions' in api.url:
146
+ submission_id = get_submission_id(api.html)
147
+ if not submission_id:
148
+ print('[red][-][/red] 提出IDが取得できませんでした')
149
+ return None
150
+
151
+ url = api.url.replace('/me', f'/{submission_id}')
152
+ print('[green][+][/green] 提出に成功しました!')
153
+ print(f'提出ID: {submission_id}, URL: {url}')
154
+ return url + '/status/json'
155
+ else:
156
+ print('[red][-][/red] 提出に失敗しました')
157
+ return None
158
+
159
+
160
+ class SubmissionStatus(NamedTuple):
161
+ status: ResultStatus
162
+ current: Optional[int]
163
+ total: Optional[int]
164
+ is_finished: bool
165
+
166
+
167
+ def parse_submission_status_json(data: Dict) -> SubmissionStatus:
168
+ html_content = data.get('Html', '')
169
+ interval = data.get('Interval', None)
170
+
171
+ soup = bs(html_content, 'html.parser')
172
+ span = soup.find('span', {'class': 'label'})
173
+ status_text = span.text.strip()
174
+
175
+ current, total = None, None
176
+ is_finished = interval is None
177
+
178
+ match = re.search(r'(\d+)/(\d+)', status_text)
179
+ if match:
180
+ current = int(match.group(1))
181
+ total = int(match.group(2))
182
+
183
+ status_mapping = {
184
+ 'AC': ResultStatus.AC,
185
+ 'WA': ResultStatus.WA,
186
+ 'TLE': ResultStatus.TLE,
187
+ 'MLE': ResultStatus.MLE,
188
+ 'RE': ResultStatus.RE,
189
+ 'CE': ResultStatus.CE,
190
+ 'WJ': ResultStatus.WJ,
191
+ }
192
+ status = next(
193
+ (status_mapping[key] for key in status_mapping if key in status_text),
194
+ ResultStatus.WJ,
195
+ )
196
+
197
+ return SubmissionStatus(
198
+ status=status, current=current, total=total, is_finished=is_finished
199
+ )
200
+
201
+
202
+ def print_status_submission(
203
+ api_url: str,
204
+ path: str,
205
+ session: requests.Session,
206
+ ) -> None:
207
+ progress = Progress(
208
+ SpinnerColumn(style='white', spinner_name='circleHalves'),
209
+ TextColumn('{task.description}'),
210
+ SpinnerColumn(style='white', spinner_name='simpleDots'),
211
+ BarColumn(),
212
+ )
213
+
214
+ with Status('ジャッジ待機中', spinner='dots'):
215
+ for _ in range(15):
216
+ time.sleep(1)
217
+ data = session.get(api_url).json()
218
+ status = parse_submission_status_json(data)
219
+ if status.total or status.current:
220
+ break
221
+ else:
222
+ print('[red][-][/] 15秒待ってもジャッジが開始されませんでした')
223
+ return
224
+
225
+ total = status.total or 0
226
+ task_id = progress.add_task(description='ジャッジ中', total=total)
227
+
228
+ test_info = TestInformation(
229
+ lang=detect_language(path),
230
+ sourcename=path,
231
+ case_number=total,
232
+ )
233
+
234
+ with Live(create_renderable_test_info(test_info, progress)) as live:
235
+ current = 0
236
+ while not status.is_finished:
237
+ time.sleep(1)
238
+ data = session.get(api_url).json()
239
+ status = parse_submission_status_json(data)
240
+ current = status.current or current or 0
241
+
242
+ test_info.summary = status.status
243
+ test_info.results = [ResultStatus.AC] * current
244
+
245
+ progress.update(task_id, completed=current)
246
+ live.update(create_renderable_test_info(test_info, progress))
247
+
248
+ test_info.summary = status.status
249
+ test_info.results = [ResultStatus.AC] * total
250
+
251
+ progress.update(task_id, description='ジャッジ完了', completed=total)
252
+ live.update(create_renderable_test_info(test_info, progress))
253
+
254
+
255
+ def submit_source(path: str, no_test: bool, no_feedback: bool) -> None:
256
+ session = load_session()
257
+ if not validate_session(session):
258
+ print('[red][-][/] ログインしていません.')
259
+ login()
260
+ if not validate_session(session):
261
+ print('[red][-][/] ログインに失敗しました.')
262
+ return
263
+
264
+ html_files = [file for file in os.listdir('.') if file.endswith('.html')]
265
+ if not html_files:
266
+ print(
267
+ '問題のファイルが見つかりません \n問題のファイルが存在するディレクトリーに移動してから実行してください'
268
+ )
269
+ return
270
+
271
+ with open(html_files[0], 'r') as file:
272
+ problem = ProblemHTML(file.read())
273
+
274
+ lcases = problem.load_labeled_testcase()
275
+ url = problem.link
276
+
277
+ test = TestRunner(path, lcases)
278
+ list(test)
279
+ print(create_renderable_test_info(test.info))
280
+
281
+ if test.info.summary != ResultStatus.AC and not no_test:
282
+ print('[red][-][/] サンプルケースが AC していないので提出できません')
283
+ return
284
+
285
+ api_status_link = post_source(path, url, session)
286
+ if api_status_link is None:
287
+ return
288
+
289
+ if not no_feedback:
290
+ print_status_submission(api_status_link, path, session)
291
+
292
+
293
+ @click.command(short_help='ソースを提出')
294
+ @add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
295
+ @click.option('--no-test', is_flag=True, default=False, help='テストをスキップ')
296
+ @click.option(
297
+ '--no-feedback', is_flag=True, default=False, help='フィードバックをスキップ'
298
+ )
299
+ def submit(files, no_test, no_feedback):
300
+ """指定したファイルをAtCoderへ提出します。"""
301
+ for path in files:
302
+ submit_source(path, no_test, no_feedback)