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/download.py +251 -240
- atcdr/generate.py +184 -193
- atcdr/login.py +133 -0
- atcdr/logout.py +24 -0
- atcdr/main.py +24 -17
- atcdr/markdown.py +22 -22
- atcdr/open.py +30 -30
- atcdr/submit.py +297 -0
- atcdr/test.py +382 -244
- atcdr/util/execute.py +52 -52
- atcdr/util/filetype.py +81 -67
- atcdr/util/gpt.py +102 -96
- atcdr/util/parse.py +206 -0
- atcdr/util/problem.py +94 -91
- atcdr/util/session.py +140 -0
- atcoderstudybooster-0.3.1.dist-info/METADATA +205 -0
- atcoderstudybooster-0.3.1.dist-info/RECORD +21 -0
- {atcoderstudybooster-0.2.dist-info → atcoderstudybooster-0.3.1.dist-info}/WHEEL +1 -1
- atcdr/util/cost.py +0 -120
- atcoderstudybooster-0.2.dist-info/METADATA +0 -96
- atcoderstudybooster-0.2.dist-info/RECORD +0 -17
- {atcoderstudybooster-0.2.dist-info → atcoderstudybooster-0.3.1.dist-info}/entry_points.txt +0 -0
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.
|
8
|
+
from atcdr.util.parse import ProblemHTML
|
9
9
|
|
10
10
|
|
11
11
|
def save_markdown(html_path: str, lang: str) -> None:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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.
|
7
|
+
from atcdr.util.parse import ProblemHTML
|
8
8
|
|
9
9
|
|
10
10
|
def open_html(file: str) -> None:
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
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
|
+
)
|