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