AtCoderStudyBooster 0.1.1__py3-none-any.whl → 0.21__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 +117 -94
- atcdr/generate.py +76 -23
- atcdr/main.py +5 -0
- atcdr/markdown.py +39 -0
- atcdr/open.py +27 -20
- atcdr/test.py +61 -43
- atcdr/util/execute.py +63 -0
- atcdr/util/{filename.py → filetype.py} +2 -48
- atcdr/util/problem.py +36 -32
- {atcoderstudybooster-0.1.1.dist-info → atcoderstudybooster-0.21.dist-info}/METADATA +3 -3
- atcoderstudybooster-0.21.dist-info/RECORD +17 -0
- atcoderstudybooster-0.1.1.dist-info/RECORD +0 -15
- {atcoderstudybooster-0.1.1.dist-info → atcoderstudybooster-0.21.dist-info}/WHEEL +0 -0
- {atcoderstudybooster-0.1.1.dist-info → atcoderstudybooster-0.21.dist-info}/entry_points.txt +0 -0
atcdr/download.py
CHANGED
@@ -3,11 +3,22 @@ import re
|
|
3
3
|
import time
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from enum import Enum
|
6
|
-
from typing import Callable, List,
|
6
|
+
from typing import Callable, List, Optional, Union, cast
|
7
7
|
|
8
|
+
import questionary as q
|
8
9
|
import requests
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.prompt import IntPrompt, Prompt
|
9
12
|
|
10
|
-
from atcdr.util.
|
13
|
+
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
14
|
+
from atcdr.util.problem import (
|
15
|
+
get_title_from_html,
|
16
|
+
make_problem_markdown,
|
17
|
+
repair_html,
|
18
|
+
title_to_filename,
|
19
|
+
)
|
20
|
+
|
21
|
+
console = Console()
|
11
22
|
|
12
23
|
|
13
24
|
class Diff(Enum):
|
@@ -37,63 +48,42 @@ def get_problem_html(problem: Problem) -> Optional[str]:
|
|
37
48
|
if response.status_code == 200:
|
38
49
|
return response.text
|
39
50
|
elif response.status_code == 429:
|
40
|
-
print(
|
41
|
-
f'[Error{response.status_code}]
|
51
|
+
console.print(
|
52
|
+
f'[bold yellow][Error {response.status_code}][/bold yellow] 再試行します。abc{problem.number} {problem.difficulty.value}'
|
42
53
|
)
|
43
54
|
time.sleep(retry_wait)
|
44
55
|
elif 300 <= response.status_code < 400:
|
45
|
-
print(
|
46
|
-
f'[
|
56
|
+
console.print(
|
57
|
+
f'[bold yellow][Error {response.status_code}][/bold yellow] リダイレクトが発生しました。abc{problem.number} {problem.difficulty.value}'
|
47
58
|
)
|
48
59
|
elif 400 <= response.status_code < 500:
|
49
|
-
print(
|
50
|
-
f'[Error{response.status_code}] 問題が見つかりません。abc{problem.number} {problem.difficulty.value}'
|
60
|
+
console.print(
|
61
|
+
f'[bold red][Error {response.status_code}][/bold red] 問題が見つかりません。abc{problem.number} {problem.difficulty.value}'
|
51
62
|
)
|
52
63
|
break
|
53
64
|
elif 500 <= response.status_code < 600:
|
54
|
-
print(
|
55
|
-
f'[Error{response.status_code}] サーバーエラーが発生しました。abc{problem.number} {problem.difficulty.value}'
|
65
|
+
console.print(
|
66
|
+
f'[bold red][Error {response.status_code}][/bold red] サーバーエラーが発生しました。abc{problem.number} {problem.difficulty.value}'
|
56
67
|
)
|
57
68
|
break
|
58
69
|
else:
|
59
|
-
print(
|
60
|
-
f'[Error{response.status_code}] abc{problem.number} {problem.difficulty.value}に対応するHTMLファイルを取得できませんでした。'
|
70
|
+
console.print(
|
71
|
+
f'[bold red][Error {response.status_code}][/bold red] abc{problem.number} {problem.difficulty.value}に対応するHTMLファイルを取得できませんでした。'
|
61
72
|
)
|
62
73
|
break
|
63
74
|
return None
|
64
75
|
|
65
76
|
|
66
|
-
def repair_html(html: str) -> str:
|
67
|
-
html = html.replace('//img.atcoder.jp', 'https://img.atcoder.jp')
|
68
|
-
html = html.replace(
|
69
|
-
'<meta http-equiv="Content-Language" content="en">',
|
70
|
-
'<meta http-equiv="Content-Language" content="ja">',
|
71
|
-
)
|
72
|
-
html = html.replace('LANG = "en"', 'LANG="ja"')
|
73
|
-
return html
|
74
|
-
|
75
|
-
|
76
|
-
def get_title_from_html(html: str) -> Optional[str]:
|
77
|
-
title_match: Optional[Match[str]] = re.search(
|
78
|
-
r'<title>(?:.*?-\s*)?([^<]*)</title>', html, re.IGNORECASE | re.DOTALL
|
79
|
-
)
|
80
|
-
if title_match:
|
81
|
-
title: str = title_match.group(1).replace(' ', '')
|
82
|
-
title = re.sub(r'[\\/*?:"<>| ]', '', title)
|
83
|
-
return title
|
84
|
-
return None
|
85
|
-
|
86
|
-
|
87
77
|
def save_file(file_path: str, html: str) -> None:
|
88
78
|
with open(file_path, 'w', encoding='utf-8') as file:
|
89
79
|
file.write(html)
|
90
|
-
print(f'[+] ファイルを保存しました :{file_path}')
|
80
|
+
console.print(f'[bold green][+][/bold green] ファイルを保存しました :{file_path}')
|
91
81
|
|
92
82
|
|
93
83
|
def mkdir(path: str) -> None:
|
94
84
|
if not os.path.exists(path):
|
95
85
|
os.makedirs(path)
|
96
|
-
print(f'[+] フォルダー: {path} を作成しました')
|
86
|
+
console.print(f'[bold green][+][/bold green] フォルダー: {path} を作成しました')
|
97
87
|
|
98
88
|
|
99
89
|
class GenerateMode:
|
@@ -117,17 +107,20 @@ def generate_problem_directory(
|
|
117
107
|
continue
|
118
108
|
|
119
109
|
title = get_title_from_html(html)
|
120
|
-
if title
|
121
|
-
print('[Error] タイトルが取得できませんでした')
|
110
|
+
if not title:
|
111
|
+
console.print('[bold red][Error][/bold red] タイトルが取得できませんでした')
|
122
112
|
title = f'problem{problem.number}{problem.difficulty.value}'
|
123
113
|
|
114
|
+
title = title_to_filename(title)
|
115
|
+
|
124
116
|
mkdir(dir_path)
|
125
117
|
repaired_html = repair_html(html)
|
126
118
|
|
127
|
-
html_path = os.path.join(dir_path,
|
119
|
+
html_path = os.path.join(dir_path, title + FILE_EXTENSIONS[Lang.HTML])
|
128
120
|
save_file(html_path, repaired_html)
|
129
121
|
md = make_problem_markdown(html, 'ja')
|
130
|
-
|
122
|
+
md_path = os.path.join(dir_path, title + FILE_EXTENSIONS[Lang.MARKDOWN])
|
123
|
+
save_file(md_path, md)
|
131
124
|
|
132
125
|
|
133
126
|
def parse_range(range_str: str) -> List[int]:
|
@@ -136,11 +129,11 @@ def parse_range(range_str: str) -> List[int]:
|
|
136
129
|
start, end = map(int, match.groups())
|
137
130
|
return list(range(start, end + 1))
|
138
131
|
else:
|
139
|
-
raise ValueError('
|
132
|
+
raise ValueError('数字の範囲の形式が間違っています')
|
140
133
|
|
141
134
|
|
142
135
|
def parse_diff_range(range_str: str) -> List[Diff]:
|
143
|
-
match = re.match(r'^([A-
|
136
|
+
match = re.match(r'^([A-Z])\.\.([A-Z])$', range_str)
|
144
137
|
if match:
|
145
138
|
start, end = match.groups()
|
146
139
|
start_index = ord(start) - ord('A')
|
@@ -150,7 +143,7 @@ def parse_diff_range(range_str: str) -> List[Diff]:
|
|
150
143
|
raise ValueError('A..C の形式になっていません')
|
151
144
|
|
152
145
|
|
153
|
-
def convert_arg(arg:
|
146
|
+
def convert_arg(arg: str) -> Union[List[int], List[Diff]]:
|
154
147
|
if isinstance(arg, int):
|
155
148
|
return [arg]
|
156
149
|
elif isinstance(arg, str):
|
@@ -160,7 +153,7 @@ def convert_arg(arg: Union[str, int]) -> Union[List[int], List[Diff]]:
|
|
160
153
|
return [Diff[arg]]
|
161
154
|
elif re.match(r'^\d+\.\.\d+$', arg):
|
162
155
|
return parse_range(arg)
|
163
|
-
elif re.match(r'^[A-
|
156
|
+
elif re.match(r'^[A-Z]\.\.[A-Z]$', arg):
|
164
157
|
return parse_diff_range(arg)
|
165
158
|
raise ValueError(f'{arg}は認識できません')
|
166
159
|
|
@@ -173,13 +166,87 @@ def are_all_diffs(args: Union[List[int], List[Diff]]) -> bool:
|
|
173
166
|
return all(isinstance(arg, Diff) for arg in args)
|
174
167
|
|
175
168
|
|
169
|
+
def interactive_download() -> None:
|
170
|
+
CONTEST = '1. 特定のコンテストの問題を解きたい'
|
171
|
+
PRACTICE = '2. 特定の難易度の問題を集中的に練習したい'
|
172
|
+
ONE_FILE = '3. 1ファイルだけダウンロードする'
|
173
|
+
END = '4. 終了する'
|
174
|
+
|
175
|
+
choice = q.select(
|
176
|
+
message='AtCoderの問題のHTMLファイルをダウンロードします',
|
177
|
+
qmark='',
|
178
|
+
pointer='❯❯❯',
|
179
|
+
choices=[CONTEST, PRACTICE, ONE_FILE, END],
|
180
|
+
instruction='\n 十字キーで移動,[enter]で実行',
|
181
|
+
style=q.Style(
|
182
|
+
[
|
183
|
+
('question', 'fg:#2196F3 bold'),
|
184
|
+
('answer', 'fg:#FFB300 bold'),
|
185
|
+
('pointer', 'fg:#FFB300 bold'),
|
186
|
+
('highlighted', 'fg:#FFB300 bold'),
|
187
|
+
('selected', 'fg:#FFB300 bold'),
|
188
|
+
]
|
189
|
+
),
|
190
|
+
).ask()
|
191
|
+
|
192
|
+
if choice == CONTEST:
|
193
|
+
number = IntPrompt.ask(
|
194
|
+
'コンテスト番号を入力してください (例: 120)',
|
195
|
+
)
|
196
|
+
contest_diffs = list(Diff)
|
197
|
+
|
198
|
+
problems = [Problem(number, diff) for diff in contest_diffs]
|
199
|
+
|
200
|
+
generate_problem_directory('.', problems, GenerateMode.gene_path_on_num)
|
201
|
+
|
202
|
+
elif choice == PRACTICE:
|
203
|
+
diff = Prompt.ask(
|
204
|
+
'難易度を入力してください (例: A)',
|
205
|
+
)
|
206
|
+
try:
|
207
|
+
diff = Diff[diff.upper()]
|
208
|
+
except KeyError:
|
209
|
+
raise ValueError('入力された難易度が認識できません')
|
210
|
+
number_str = Prompt.ask(
|
211
|
+
'コンテスト番号または範囲を入力してください (例: 120..130)'
|
212
|
+
)
|
213
|
+
if number_str.isdigit():
|
214
|
+
contest_numbers = [int(number_str)]
|
215
|
+
elif re.match(r'^\d+\.\.\d+$', number_str):
|
216
|
+
contest_numbers = parse_range(number_str)
|
217
|
+
else:
|
218
|
+
raise ValueError('数字の範囲の形式が間違っています')
|
219
|
+
|
220
|
+
problems = [Problem(number, diff) for number in contest_numbers]
|
221
|
+
|
222
|
+
generate_problem_directory('.', problems, GenerateMode.gene_path_on_diff)
|
223
|
+
|
224
|
+
elif choice == ONE_FILE:
|
225
|
+
contest_number = IntPrompt.ask(
|
226
|
+
'コンテスト番号を入力してください (例: 120)',
|
227
|
+
)
|
228
|
+
difficulty = Prompt.ask(
|
229
|
+
'難易度を入力してください (例: A)', choices=[d.name for d in Diff]
|
230
|
+
)
|
231
|
+
|
232
|
+
difficulty = difficulty.upper().strip()
|
233
|
+
|
234
|
+
problem = Problem(contest_number, Diff[difficulty])
|
235
|
+
generate_problem_directory('.', [problem], GenerateMode.gene_path_on_num)
|
236
|
+
|
237
|
+
elif choice == END:
|
238
|
+
console.print('終了します', style='bold red')
|
239
|
+
else:
|
240
|
+
console.print('無効な選択です', style='bold red')
|
241
|
+
|
242
|
+
|
176
243
|
def download(
|
177
244
|
first: Union[str, int, None] = None,
|
178
245
|
second: Union[str, int, None] = None,
|
179
246
|
base_path: str = '.',
|
180
247
|
) -> None:
|
181
248
|
if first is None:
|
182
|
-
|
249
|
+
interactive_download()
|
183
250
|
return
|
184
251
|
|
185
252
|
first_args = convert_arg(str(first))
|
@@ -187,9 +254,9 @@ def download(
|
|
187
254
|
if isinstance(first, Diff):
|
188
255
|
raise ValueError(
|
189
256
|
"""難易度だけでなく, 問題番号も指定してコマンドを実行してください.
|
190
|
-
|
191
|
-
|
192
|
-
|
257
|
+
例 atcdr -d A 120 : A問題の120をダウンロードます
|
258
|
+
例 atcdr -d A 120..130 : A問題の120から130をダウンロードます
|
259
|
+
"""
|
193
260
|
)
|
194
261
|
second_args: Union[List[int], List[Diff]] = list(Diff)
|
195
262
|
else:
|
@@ -216,51 +283,7 @@ def download(
|
|
216
283
|
else:
|
217
284
|
raise ValueError(
|
218
285
|
"""次のような形式で問題を指定してください
|
219
|
-
|
220
|
-
|
221
|
-
|
286
|
+
例 atcdr -d A 120..130 : A問題の120から130をダウンロードします
|
287
|
+
例 atcdr -d 120 : ABCのコンテストの問題をダウンロードします
|
288
|
+
"""
|
222
289
|
)
|
223
|
-
|
224
|
-
|
225
|
-
def main() -> None:
|
226
|
-
print('AtCoderの問題のHTMLファイルをダウンロードします')
|
227
|
-
print(
|
228
|
-
"""
|
229
|
-
1. 番号の範囲を指定してダウンロードする
|
230
|
-
2. 1ファイルだけダウンロードする
|
231
|
-
q: 終了
|
232
|
-
"""
|
233
|
-
)
|
234
|
-
|
235
|
-
choice = input('選択してください: ')
|
236
|
-
|
237
|
-
if choice == '1':
|
238
|
-
start_end = input(
|
239
|
-
'開始と終了のコンテストの番号をスペースで区切って指定してください (例: 223 230): '
|
240
|
-
)
|
241
|
-
start, end = map(int, start_end.split(' '))
|
242
|
-
difficulty = Diff[
|
243
|
-
input(
|
244
|
-
'ダウンロードする問題の難易度を指定してください (例: A, B, C): '
|
245
|
-
).upper()
|
246
|
-
]
|
247
|
-
problem_list = [Problem(number, difficulty) for number in range(start, end + 1)]
|
248
|
-
generate_problem_directory('.', problem_list, GenerateMode.gene_path_on_diff)
|
249
|
-
elif choice == '2':
|
250
|
-
number = int(input('コンテストの番号を指定してください: '))
|
251
|
-
difficulty = Diff[
|
252
|
-
input(
|
253
|
-
'ダウンロードする問題の難易度を指定してください (例: A, B, C): '
|
254
|
-
).upper()
|
255
|
-
]
|
256
|
-
generate_problem_directory(
|
257
|
-
'.', [Problem(number, difficulty)], GenerateMode.gene_path_on_diff
|
258
|
-
)
|
259
|
-
elif choice == 'q':
|
260
|
-
print('終了します')
|
261
|
-
else:
|
262
|
-
print('無効な選択です')
|
263
|
-
|
264
|
-
|
265
|
-
if __name__ == '__main__':
|
266
|
-
main()
|
atcdr/generate.py
CHANGED
@@ -2,17 +2,20 @@ import json
|
|
2
2
|
import os
|
3
3
|
import re
|
4
4
|
|
5
|
+
from rich.console import Console
|
6
|
+
|
5
7
|
from atcdr.test import (
|
8
|
+
LabeledTestCaseResult,
|
6
9
|
ResultStatus,
|
7
10
|
create_testcases_from_html,
|
8
11
|
judge_code_from,
|
9
|
-
|
12
|
+
render_results,
|
10
13
|
)
|
11
|
-
from atcdr.util.
|
14
|
+
from atcdr.util.execute import execute_files
|
15
|
+
from atcdr.util.filetype import (
|
12
16
|
FILE_EXTENSIONS,
|
13
17
|
Filename,
|
14
18
|
Lang,
|
15
|
-
execute_files,
|
16
19
|
lang2str,
|
17
20
|
str2lang,
|
18
21
|
)
|
@@ -26,7 +29,38 @@ def get_code_from_gpt_output(output: str) -> str:
|
|
26
29
|
return match.group(1) if match else ''
|
27
30
|
|
28
31
|
|
32
|
+
def render_result_for_GPT(lresult: LabeledTestCaseResult) -> str:
|
33
|
+
output = f'{lresult.label} of Test:\n'
|
34
|
+
result = lresult.result
|
35
|
+
testcase = lresult.testcase
|
36
|
+
|
37
|
+
if result.passed == ResultStatus.AC:
|
38
|
+
output += 'Accepted !! \n'
|
39
|
+
|
40
|
+
elif result.passed == ResultStatus.WA:
|
41
|
+
output += (
|
42
|
+
f'Wrong Answer\n'
|
43
|
+
f'Output:\n{result.output}\n'
|
44
|
+
f'Expected Output:\n{testcase.output}\n'
|
45
|
+
)
|
46
|
+
|
47
|
+
elif result.passed == ResultStatus.RE:
|
48
|
+
output += f'[RE] Runtime Error\n Output:\n{result.output}'
|
49
|
+
|
50
|
+
elif result.passed == ResultStatus.TLE:
|
51
|
+
output += '[TLE] Time Limit Exceeded\n'
|
52
|
+
|
53
|
+
elif result.passed == ResultStatus.CE:
|
54
|
+
output += f'[CE] Compile Error\n Output:\n{result.output}'
|
55
|
+
|
56
|
+
elif result.passed == ResultStatus.MLE:
|
57
|
+
output += '[ME] Memory Limit Exceeded\n'
|
58
|
+
|
59
|
+
return output
|
60
|
+
|
61
|
+
|
29
62
|
def generate_code(file: Filename, lang: Lang) -> None:
|
63
|
+
console = Console()
|
30
64
|
with open(file, 'r') as f:
|
31
65
|
html_content = f.read()
|
32
66
|
md = make_problem_markdown(html_content, 'en')
|
@@ -36,20 +70,25 @@ def generate_code(file: Filename, lang: Lang) -> None:
|
|
36
70
|
gpt = ChatGPT(
|
37
71
|
system_prompt=f"""You are an excellent programmer. You solve problems in competitive programming.When a user provides you with a problem from a programming contest called AtCoder, including the Problem,Constraints, Input, Output, Input Example, and Output Example, please carefully consider these and solve the problem.Make sure that your output code block contains no more than two blocks. Pay close attention to the Input, Input Example, Output, and Output Example.Create the solution in {lang2str(lang)}.""",
|
38
72
|
)
|
73
|
+
with console.status(f'{gpt.model.value}がコードを生成しています...'):
|
74
|
+
reply = gpt.tell(md)
|
39
75
|
|
40
|
-
reply = gpt.tell(md)
|
41
76
|
code = get_code_from_gpt_output(reply)
|
42
|
-
print(f'AI利用にかかったAPIコスト: {gpt.sum_cost}')
|
43
77
|
|
44
78
|
saved_filename = (
|
45
79
|
os.path.splitext(file)[0] + f'_by_{gpt.model.value}' + FILE_EXTENSIONS[lang]
|
46
80
|
)
|
47
81
|
with open(saved_filename, 'w') as f:
|
48
|
-
print(
|
82
|
+
console.print(
|
83
|
+
f'[green][+][/green] {gpt.model.value} の出力したコードを保存しました:{f.name}'
|
84
|
+
)
|
49
85
|
f.write(code)
|
50
86
|
|
87
|
+
console.print(f'[info] AI利用にかかったAPIコスト: {gpt.sum_cost}')
|
88
|
+
|
51
89
|
|
52
90
|
def generate_template(file: Filename, lang: Lang) -> None:
|
91
|
+
console = Console()
|
53
92
|
with open(file, 'r') as f:
|
54
93
|
html_content = f.read()
|
55
94
|
md = make_problem_markdown(html_content, 'en')
|
@@ -70,17 +109,22 @@ The user will provide a problem from a programming contest called AtCoder. This
|
|
70
109
|
|
71
110
|
You must not solve the problem. Please faithfully reproduce the variable names defined in the problem.
|
72
111
|
"""
|
73
|
-
|
112
|
+
with console.status(f'{lang2str(lang)}のテンプレートを生成しています...'):
|
113
|
+
reply = gpt.tell(md + propmpt)
|
74
114
|
code = get_code_from_gpt_output(reply)
|
75
|
-
print(f'AI利用にかかったAPIコスト:{gpt.sum_cost}')
|
76
115
|
|
77
116
|
savaed_filename = os.path.splitext(file)[0] + FILE_EXTENSIONS[lang]
|
78
|
-
with open(savaed_filename, '
|
79
|
-
print(
|
117
|
+
with open(savaed_filename, 'x') as f:
|
118
|
+
console.print(
|
119
|
+
f'[green][+][/green] テンプレートファイルを作成 :{savaed_filename}'
|
120
|
+
)
|
80
121
|
f.write(code)
|
81
122
|
|
123
|
+
console.print(f'[info] AI利用にかかったAPIコスト: {gpt.sum_cost}')
|
124
|
+
|
82
125
|
|
83
126
|
def solve_problem(file: Filename, lang: Lang) -> None:
|
127
|
+
console = Console()
|
84
128
|
with open(file, 'r') as f:
|
85
129
|
html_content = f.read()
|
86
130
|
md = make_problem_markdown(html_content, 'en')
|
@@ -94,9 +138,16 @@ def solve_problem(file: Filename, lang: Lang) -> None:
|
|
94
138
|
|
95
139
|
file_without_ext = os.path.splitext(file)[0]
|
96
140
|
|
97
|
-
reply = gpt.tell(md)
|
98
|
-
|
99
141
|
for i in range(1, 4):
|
142
|
+
with console.status(f'{i}回目のコード生成 (by {gpt.model.value})...'):
|
143
|
+
test_report = ''
|
144
|
+
if i == 1:
|
145
|
+
reply = gpt.tell(md)
|
146
|
+
else:
|
147
|
+
reply = gpt.tell(f"""The following is the test report for the code you provided:
|
148
|
+
{test_report}
|
149
|
+
Please provide an updated version of the code in {lang2str(lang)}.""")
|
150
|
+
|
100
151
|
code = get_code_from_gpt_output(reply)
|
101
152
|
|
102
153
|
saved_filename = (
|
@@ -106,25 +157,25 @@ def solve_problem(file: Filename, lang: Lang) -> None:
|
|
106
157
|
+ FILE_EXTENSIONS[lang]
|
107
158
|
)
|
108
159
|
with open(saved_filename, 'w') as f:
|
109
|
-
print(
|
160
|
+
console.print(
|
161
|
+
f'[green][+][/green] {gpt.model.value} の出力したコードを保存しました:{f.name}'
|
162
|
+
)
|
110
163
|
f.write(code)
|
111
164
|
|
112
165
|
labeled_results = judge_code_from(labeled_cases, saved_filename)
|
113
|
-
test_report = '\n'.join(
|
166
|
+
test_report = '\n'.join(
|
167
|
+
render_result_for_GPT(lresult) for lresult in labeled_results
|
168
|
+
)
|
114
169
|
|
115
|
-
|
116
|
-
|
170
|
+
console.rule(f'{i}回目のコード生成でのテスト結果')
|
171
|
+
render_results(saved_filename, labeled_results)
|
117
172
|
|
118
173
|
if all(
|
119
174
|
labeled_result.result.passed == ResultStatus.AC
|
120
175
|
for labeled_result in labeled_results
|
121
176
|
):
|
122
|
-
print('コードのテストに成功!')
|
177
|
+
console.print('[green]コードのテストに成功![/green]')
|
123
178
|
break
|
124
|
-
else:
|
125
|
-
reply = gpt.tell(f"""The following is the test report for the code you provided:
|
126
|
-
{test_report}
|
127
|
-
Please provide an updated version of the code in {lang2str(lang)}.""")
|
128
179
|
|
129
180
|
with open(
|
130
181
|
'log_'
|
@@ -133,9 +184,11 @@ Please provide an updated version of the code in {lang2str(lang)}.""")
|
|
133
184
|
+ FILE_EXTENSIONS[Lang.JSON],
|
134
185
|
'w',
|
135
186
|
) as f:
|
136
|
-
print(
|
187
|
+
console.print(
|
188
|
+
f'[green][+][/green] {gpt.model.value}の出力のログを保存しました:{f.name}'
|
189
|
+
)
|
137
190
|
f.write(json.dumps(gpt.messages, indent=2))
|
138
|
-
print(f'AI利用にかかったAPIコスト:{gpt.sum_cost}')
|
191
|
+
console.print(f'AI利用にかかったAPIコスト:{gpt.sum_cost}')
|
139
192
|
return
|
140
193
|
|
141
194
|
|
atcdr/main.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
from importlib.metadata import metadata
|
2
2
|
|
3
3
|
import fire # type: ignore
|
4
|
+
from rich.traceback import install
|
4
5
|
|
5
6
|
from atcdr.download import download
|
6
7
|
from atcdr.generate import generate
|
8
|
+
from atcdr.markdown import markdown
|
7
9
|
from atcdr.open import open_files
|
8
10
|
from atcdr.test import test
|
9
11
|
|
@@ -22,12 +24,15 @@ MAP_COMMANDS: dict = {
|
|
22
24
|
'o': open_files,
|
23
25
|
'generate': generate,
|
24
26
|
'g': generate,
|
27
|
+
'markdown': markdown,
|
28
|
+
'md': markdown,
|
25
29
|
'--version': get_version,
|
26
30
|
'-v': get_version,
|
27
31
|
}
|
28
32
|
|
29
33
|
|
30
34
|
def main():
|
35
|
+
install()
|
31
36
|
fire.Fire(MAP_COMMANDS)
|
32
37
|
|
33
38
|
|
atcdr/markdown.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.markdown import Markdown
|
5
|
+
|
6
|
+
from atcdr.util.execute import execute_files
|
7
|
+
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
8
|
+
from atcdr.util.problem import make_problem_markdown
|
9
|
+
|
10
|
+
|
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]
|
18
|
+
|
19
|
+
with open(md_path, 'w', encoding='utf-8') as f:
|
20
|
+
f.write(md)
|
21
|
+
console.print('[green][+][/green] Markdownファイルを作成しました.')
|
22
|
+
|
23
|
+
|
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))
|
29
|
+
|
30
|
+
|
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])
|
atcdr/open.py
CHANGED
@@ -1,35 +1,42 @@
|
|
1
|
-
import webbrowser
|
1
|
+
import webbrowser # noqa: I001
|
2
|
+
from rich.panel import Panel
|
3
|
+
from rich.console import Console
|
2
4
|
|
3
|
-
from
|
4
|
-
from
|
5
|
-
|
6
|
-
from atcdr.util.filename import Lang, execute_files
|
7
|
-
|
8
|
-
|
9
|
-
def find_link_from(html: str) -> str | None:
|
10
|
-
soup = bs(html, 'html.parser')
|
11
|
-
meta_tag = soup.find('meta', property='og:url')
|
12
|
-
if isinstance(meta_tag, Tag) and 'content' in meta_tag.attrs:
|
13
|
-
content = meta_tag['content']
|
14
|
-
if isinstance(content, list):
|
15
|
-
return content[0] # 必要に応じて、最初の要素を返す
|
16
|
-
return content
|
17
|
-
return None
|
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
|
18
8
|
|
19
9
|
|
20
10
|
def open_html(file: str) -> None:
|
11
|
+
console = Console()
|
21
12
|
try:
|
22
13
|
with open(file, 'r') as f:
|
23
14
|
html_content = f.read()
|
24
15
|
except FileNotFoundError:
|
25
|
-
print(
|
16
|
+
console.print(
|
17
|
+
Panel(
|
18
|
+
f"{file}' [red]が見つかりません[/]",
|
19
|
+
border_style='red',
|
20
|
+
)
|
21
|
+
)
|
26
22
|
return
|
27
23
|
|
28
|
-
url =
|
24
|
+
url = find_link_from_html(html_content)
|
29
25
|
if url:
|
30
|
-
webbrowser.
|
26
|
+
webbrowser.open_new_tab(url)
|
27
|
+
console.print(
|
28
|
+
Panel(
|
29
|
+
f'[green]URLを開きました[/] {url}',
|
30
|
+
border_style='green',
|
31
|
+
)
|
32
|
+
)
|
31
33
|
else:
|
32
|
-
print(
|
34
|
+
console.print(
|
35
|
+
Panel(
|
36
|
+
f'{file} [yellow]にURLが見つかりませんでした[/]',
|
37
|
+
border_style='yellow',
|
38
|
+
)
|
39
|
+
)
|
33
40
|
|
34
41
|
|
35
42
|
def open_files(*args: str) -> None:
|
atcdr/test.py
CHANGED
@@ -6,13 +6,15 @@ from dataclasses import dataclass
|
|
6
6
|
from enum import Enum
|
7
7
|
from typing import Callable, Dict, List, Optional, Union
|
8
8
|
|
9
|
-
import colorama
|
10
9
|
from bs4 import BeautifulSoup as bs
|
11
|
-
from
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.markup import escape
|
12
|
+
from rich.panel import Panel
|
13
|
+
from rich.table import Table
|
14
|
+
from rich.text import Text
|
12
15
|
|
13
|
-
from atcdr.util.
|
14
|
-
|
15
|
-
colorama.init(autoreset=True)
|
16
|
+
from atcdr.util.execute import execute_files
|
17
|
+
from atcdr.util.filetype import FILE_EXTENSIONS, SOURCE_LANGUAGES, Lang
|
16
18
|
|
17
19
|
|
18
20
|
@dataclass
|
@@ -137,8 +139,6 @@ def run_c(path: str, case: TestCase) -> TestCaseResult:
|
|
137
139
|
return TestCaseResult(
|
138
140
|
output=compile_result.stderr, executed_time=None, passed=ResultStatus.CE
|
139
141
|
)
|
140
|
-
if compile_result.stderr:
|
141
|
-
print(f'コンパイラーからのメッセージ\n{compile_result.stderr}')
|
142
142
|
return run_code([exec_path], case)
|
143
143
|
|
144
144
|
|
@@ -152,8 +152,6 @@ def run_cpp(path: str, case: TestCase) -> TestCaseResult:
|
|
152
152
|
return TestCaseResult(
|
153
153
|
output=compile_result.stderr, executed_time=None, passed=ResultStatus.CE
|
154
154
|
)
|
155
|
-
if compile_result.stderr:
|
156
|
-
print(f'コンパイラーからのメッセージ\n{compile_result.stderr}')
|
157
155
|
return run_code([exec_path], case)
|
158
156
|
|
159
157
|
|
@@ -167,8 +165,6 @@ def run_rust(path: str, case: TestCase) -> TestCaseResult:
|
|
167
165
|
return TestCaseResult(
|
168
166
|
output=compile_result.stderr, executed_time=None, passed=ResultStatus.CE
|
169
167
|
)
|
170
|
-
if compile_result.stderr:
|
171
|
-
print(f'コンパイラーからのメッセージ\n{compile_result.stderr}')
|
172
168
|
return run_code([exec_path], case)
|
173
169
|
|
174
170
|
|
@@ -221,36 +217,61 @@ def judge_code_from(
|
|
221
217
|
]
|
222
218
|
|
223
219
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
output = f'{Fore.CYAN}{lresult.label} of Test:\n'
|
230
|
-
result = lresult.result
|
231
|
-
testcase = lresult.testcase
|
232
|
-
|
233
|
-
if result.passed == ResultStatus.AC:
|
234
|
-
output += (
|
235
|
-
Fore.GREEN + f'{CHECK_MARK} Accepted !! Time: {result.executed_time} ms\n'
|
236
|
-
)
|
237
|
-
elif result.passed == ResultStatus.WA:
|
238
|
-
output += (
|
239
|
-
Fore.RED
|
240
|
-
+ f'{CROSS_MARK} Wrong Answer ! Time: {result.executed_time} ms\nOutput:\n{result.output}\nExpected Output:\n{testcase.output}\n'
|
241
|
-
)
|
242
|
-
elif result.passed == ResultStatus.RE:
|
243
|
-
output += Fore.YELLOW + f'[RE] Runtime Error\n Output:\n{result.output}'
|
244
|
-
elif result.passed == ResultStatus.TLE:
|
245
|
-
output += Fore.YELLOW + '[TLE] Time Limit Exceeded\n'
|
246
|
-
elif result.passed == ResultStatus.CE:
|
247
|
-
output += Fore.YELLOW + f'[CE] Compile Error\n Output:\n{result.output}'
|
248
|
-
elif result.passed == ResultStatus.MLE:
|
249
|
-
output += Fore.YELLOW + '[ME] Memory Limit Exceeded\n'
|
220
|
+
class CustomFormatStyle(Enum):
|
221
|
+
SUCCESS = 'green'
|
222
|
+
FAILURE = 'red'
|
223
|
+
WARNING = 'yellow'
|
224
|
+
INFO = 'blue'
|
250
225
|
|
251
|
-
output += Fore.RESET
|
252
226
|
|
253
|
-
|
227
|
+
def render_results(path: str, results: List[LabeledTestCaseResult]) -> None:
|
228
|
+
console = Console()
|
229
|
+
success_count = sum(
|
230
|
+
1 for result in results if result.result.passed == ResultStatus.AC
|
231
|
+
)
|
232
|
+
total_count = len(results)
|
233
|
+
|
234
|
+
# ヘッダー
|
235
|
+
header_text = Text.assemble(
|
236
|
+
f'{path}のテスト ',
|
237
|
+
(
|
238
|
+
f'{success_count}/{total_count} ',
|
239
|
+
'green' if success_count == total_count else 'red',
|
240
|
+
),
|
241
|
+
)
|
242
|
+
console.print(Panel(header_text, expand=False))
|
243
|
+
|
244
|
+
CHECK_MARK = '\u2713'
|
245
|
+
CROSS_MARK = '\u00d7'
|
246
|
+
# 各テストケースの結果表示
|
247
|
+
for i, result in enumerate(results):
|
248
|
+
if result.result.passed == ResultStatus.AC:
|
249
|
+
status_text = f'[green]{CHECK_MARK}[/] [white on green]{result.result.passed.value}[/]'
|
250
|
+
console.rule(title=f'No.{i+1} {result.label}', style='green')
|
251
|
+
console.print(f'[bold]ステータス:[/] {status_text}')
|
252
|
+
|
253
|
+
else:
|
254
|
+
status_text = f'[red]{CROSS_MARK} {result.result.passed.value}[/]'
|
255
|
+
console.rule(title=f'No.{i+1} {result.label}', style='red')
|
256
|
+
console.print(f'[bold]ステータス:[/] {status_text}')
|
257
|
+
|
258
|
+
if result.result.executed_time is not None:
|
259
|
+
console.print(f'[bold]実行時間:[/] {result.result.executed_time} ms')
|
260
|
+
|
261
|
+
table = Table(show_header=True, header_style='bold')
|
262
|
+
table.add_column('入力', style='cyan', min_width=10)
|
263
|
+
if result.result.passed != ResultStatus.AC:
|
264
|
+
table.add_column('出力', style='red', min_width=10)
|
265
|
+
table.add_column('正解の出力', style='green', min_width=10)
|
266
|
+
table.add_row(
|
267
|
+
escape(result.testcase.input),
|
268
|
+
escape(result.result.output),
|
269
|
+
escape(result.testcase.output),
|
270
|
+
)
|
271
|
+
else:
|
272
|
+
table.add_column('出力', style='green', min_width=10)
|
273
|
+
table.add_row(escape(result.testcase.input), escape(result.result.output))
|
274
|
+
console.print(table)
|
254
275
|
|
255
276
|
|
256
277
|
def run_test(path_of_code: str) -> None:
|
@@ -265,11 +286,8 @@ def run_test(path_of_code: str) -> None:
|
|
265
286
|
html = file.read()
|
266
287
|
|
267
288
|
test_cases = create_testcases_from_html(html)
|
268
|
-
print(f'{path_of_code}をテストします。\n' + '-' * 20 + '\n')
|
269
289
|
test_results = judge_code_from(test_cases, path_of_code)
|
270
|
-
|
271
|
-
|
272
|
-
print(output)
|
290
|
+
render_results(path_of_code, test_results)
|
273
291
|
|
274
292
|
|
275
293
|
def test(*args: str) -> None:
|
atcdr/util/execute.py
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Callable, List
|
3
|
+
|
4
|
+
import questionary as q
|
5
|
+
from rich import print
|
6
|
+
|
7
|
+
from atcdr.util.filetype import FILE_EXTENSIONS, Filename, Lang
|
8
|
+
|
9
|
+
|
10
|
+
def execute_files(
|
11
|
+
*args: str, func: Callable[[Filename], None], target_filetypes: List[Lang]
|
12
|
+
) -> None:
|
13
|
+
target_extensions = [FILE_EXTENSIONS[lang] for lang in target_filetypes]
|
14
|
+
|
15
|
+
files = [
|
16
|
+
file
|
17
|
+
for file in os.listdir('.')
|
18
|
+
if os.path.isfile(file) and os.path.splitext(file)[1] in target_extensions
|
19
|
+
]
|
20
|
+
|
21
|
+
if not files:
|
22
|
+
print(
|
23
|
+
'対象のファイルが見つかりません.\n対象ファイルが存在するディレクトリーに移動してから実行してください。'
|
24
|
+
)
|
25
|
+
return
|
26
|
+
|
27
|
+
if not args:
|
28
|
+
if len(files) == 1:
|
29
|
+
func(files[0])
|
30
|
+
else:
|
31
|
+
target_file = q.select(
|
32
|
+
message='複数のファイルが見つかりました.ファイルを選択してください:',
|
33
|
+
choices=[q.Choice(title=file, value=file) for file in files],
|
34
|
+
instruction='\n 十字キーで移動, [enter]で実行',
|
35
|
+
pointer='❯❯❯',
|
36
|
+
qmark='',
|
37
|
+
style=q.Style(
|
38
|
+
[
|
39
|
+
('qmark', 'fg:#2196F3 bold'),
|
40
|
+
('question', 'fg:#2196F3 bold'),
|
41
|
+
('answer', 'fg:#FFB300 bold'),
|
42
|
+
('pointer', 'fg:#FFB300 bold'),
|
43
|
+
('highlighted', 'fg:#FFB300 bold'),
|
44
|
+
('selected', 'fg:#FFB300 bold'),
|
45
|
+
]
|
46
|
+
),
|
47
|
+
).ask()
|
48
|
+
list(map(func, [target_file]))
|
49
|
+
else:
|
50
|
+
target_files = set()
|
51
|
+
for arg in args:
|
52
|
+
if arg == '*':
|
53
|
+
target_files.update(files)
|
54
|
+
elif arg.startswith('*.'):
|
55
|
+
ext = arg[1:] # ".py" のような拡張子を取得
|
56
|
+
target_files.update(file for file in files if file.endswith(ext))
|
57
|
+
else:
|
58
|
+
if arg in files:
|
59
|
+
target_files.add(arg)
|
60
|
+
else:
|
61
|
+
print(f'エラー: {arg} は存在しません。')
|
62
|
+
|
63
|
+
list(map(func, target_files))
|
@@ -1,6 +1,5 @@
|
|
1
|
-
import os
|
2
1
|
from enum import Enum
|
3
|
-
from typing import
|
2
|
+
from typing import Dict, List, TypeAlias
|
4
3
|
|
5
4
|
# ファイル名と拡張子の型エイリアスを定義
|
6
5
|
Filename: TypeAlias = str
|
@@ -72,6 +71,7 @@ def str2lang(lang: str) -> Lang:
|
|
72
71
|
'cpp': Lang.CPP,
|
73
72
|
'c++': Lang.CPP,
|
74
73
|
'csharp': Lang.CSHARP,
|
74
|
+
'cs': Lang.CSHARP,
|
75
75
|
'c#': Lang.CSHARP,
|
76
76
|
'rb': Lang.RUBY,
|
77
77
|
'ruby': Lang.RUBY,
|
@@ -89,49 +89,3 @@ def str2lang(lang: str) -> Lang:
|
|
89
89
|
|
90
90
|
def lang2str(lang: Lang) -> str:
|
91
91
|
return lang.value
|
92
|
-
|
93
|
-
|
94
|
-
def execute_files(
|
95
|
-
*args: str, func: Callable[[Filename], None], target_filetypes: List[Lang]
|
96
|
-
) -> None:
|
97
|
-
target_extensions = [FILE_EXTENSIONS[lang] for lang in target_filetypes]
|
98
|
-
|
99
|
-
files = [
|
100
|
-
file
|
101
|
-
for file in os.listdir('.')
|
102
|
-
if os.path.isfile(file) and os.path.splitext(file)[1] in target_extensions
|
103
|
-
]
|
104
|
-
|
105
|
-
if not files:
|
106
|
-
print(
|
107
|
-
'対象のファイルが見つかりません.\n対象ファイルが存在するディレクトリーに移動してから実行してください。'
|
108
|
-
)
|
109
|
-
return
|
110
|
-
|
111
|
-
if not args:
|
112
|
-
if len(files) == 1:
|
113
|
-
func(files[0])
|
114
|
-
else:
|
115
|
-
print('複数のファイルが見つかりました。以下のファイルから選択してください:')
|
116
|
-
for i, file in enumerate(files):
|
117
|
-
print(f'{i + 1}. {file}')
|
118
|
-
choice = int(input('ファイル番号を入力してください: ')) - 1
|
119
|
-
if 0 <= choice < len(files):
|
120
|
-
func(files[choice])
|
121
|
-
else:
|
122
|
-
print('無効な選択です')
|
123
|
-
else:
|
124
|
-
target_files = set()
|
125
|
-
for arg in args:
|
126
|
-
if arg == '*':
|
127
|
-
target_files.update(files)
|
128
|
-
elif arg.startswith('*.'):
|
129
|
-
ext = arg[1:] # ".py" のような拡張子を取得
|
130
|
-
target_files.update(file for file in files if file.endswith(ext))
|
131
|
-
else:
|
132
|
-
if arg in files:
|
133
|
-
target_files.add(arg)
|
134
|
-
else:
|
135
|
-
print(f'エラー: {arg} は存在しません。')
|
136
|
-
|
137
|
-
list(map(func, target_files))
|
atcdr/util/problem.py
CHANGED
@@ -1,46 +1,48 @@
|
|
1
1
|
import re
|
2
2
|
from enum import Enum
|
3
|
-
from typing import
|
3
|
+
from typing import Match, Optional
|
4
4
|
|
5
5
|
from bs4 import BeautifulSoup as bs
|
6
|
-
from bs4 import
|
6
|
+
from bs4 import Tag
|
7
7
|
from markdownify import MarkdownConverter # type: ignore
|
8
8
|
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def repair_html(html: str) -> str:
|
11
|
+
html = html.replace('//img.atcoder.jp', 'https://img.atcoder.jp')
|
12
|
+
html = html.replace(
|
13
|
+
'<meta http-equiv="Content-Language" content="en">',
|
14
|
+
'<meta http-equiv="Content-Language" content="ja">',
|
15
|
+
)
|
16
|
+
html = html.replace('LANG = "en"', 'LANG="ja"')
|
17
|
+
html = remove_unnecessary_emptylines(html)
|
18
|
+
return html
|
14
19
|
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
self.condition_part: Optional[str] = None
|
20
|
-
self.io_part: Optional[str] = None
|
21
|
-
self.test_part: Optional[list[str]] = None
|
21
|
+
def get_title_from_html(html: str) -> str:
|
22
|
+
title: Optional[Match[str]] = re.search(r'<title>([^<]*)</title>', html)
|
23
|
+
return title.group(1).strip() if title else ''
|
22
24
|
|
23
|
-
def divide_problem_part(self, task_statement: Union[Tag, NavigableString]) -> None:
|
24
|
-
if not isinstance(task_statement, Tag):
|
25
|
-
return
|
26
25
|
|
27
|
-
|
26
|
+
def title_to_filename(title: str) -> str:
|
27
|
+
title = re.sub(r'[\\/*?:"<>| !@#$%^&()+=\[\]{};,\']', '', title)
|
28
|
+
title = re.sub(r'^[A-Z]-', '', title)
|
29
|
+
return title
|
28
30
|
|
29
|
-
if len(parts) >= 2:
|
30
|
-
self.problem_part = str(parts[0])
|
31
|
-
self.condition_part = str(parts[1])
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
def find_link_from_html(html: str) -> str:
|
33
|
+
soup = bs(html, 'html.parser')
|
34
|
+
meta_tag = soup.find('meta', property='og:url')
|
35
|
+
if isinstance(meta_tag, Tag) and 'content' in meta_tag.attrs:
|
36
|
+
content = meta_tag['content']
|
37
|
+
if isinstance(content, list):
|
38
|
+
return content[0] # 必要に応じて、最初の要素を返す
|
39
|
+
return content
|
40
|
+
return ''
|
36
41
|
|
37
|
-
if len(io_parts) > 0:
|
38
|
-
self.io_part = str(
|
39
|
-
io_parts[0]
|
40
|
-
) # .find_all() はリストを返すので、str()でキャスト
|
41
42
|
|
42
|
-
|
43
|
-
|
43
|
+
class Lang(Enum):
|
44
|
+
JA = 'ja'
|
45
|
+
EN = 'en'
|
44
46
|
|
45
47
|
|
46
48
|
class CustomMarkdownConverter(MarkdownConverter):
|
@@ -57,10 +59,10 @@ def custom_markdownify(html, **options):
|
|
57
59
|
return CustomMarkdownConverter(**options).convert(html)
|
58
60
|
|
59
61
|
|
60
|
-
def remove_unnecessary_emptylines(
|
61
|
-
|
62
|
-
|
63
|
-
return
|
62
|
+
def remove_unnecessary_emptylines(text):
|
63
|
+
text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text)
|
64
|
+
text = text.strip()
|
65
|
+
return text
|
64
66
|
|
65
67
|
|
66
68
|
def abstract_problem_part(html_content: str, lang: str) -> str:
|
@@ -81,7 +83,9 @@ def abstract_problem_part(html_content: str, lang: str) -> str:
|
|
81
83
|
|
82
84
|
|
83
85
|
def make_problem_markdown(html_content: str, lang: str) -> str:
|
86
|
+
title = get_title_from_html(html_content)
|
84
87
|
problem_part = abstract_problem_part(html_content, lang)
|
85
88
|
problem_md = custom_markdownify(problem_part)
|
89
|
+
problem_md = f'# {title}\n{problem_md}'
|
86
90
|
problem_md = remove_unnecessary_emptylines(problem_md)
|
87
91
|
return problem_md
|
@@ -1,19 +1,19 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: AtCoderStudyBooster
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.21
|
4
4
|
Summary: A tool to download and manage AtCoder problems.
|
5
5
|
Project-URL: Homepage, https://github.com/yuta6/AtCoderStudyBooster
|
6
6
|
Author-email: yuta6 <46110512+yuta6@users.noreply.github.com>
|
7
7
|
License: MIT
|
8
8
|
Requires-Python: >=3.8
|
9
9
|
Requires-Dist: beautifulsoup4
|
10
|
-
Requires-Dist: colorama
|
11
10
|
Requires-Dist: fire
|
12
11
|
Requires-Dist: markdownify>=0.13.1
|
12
|
+
Requires-Dist: questionary>=2.0.1
|
13
13
|
Requires-Dist: requests
|
14
|
+
Requires-Dist: rich>=13.7.1
|
14
15
|
Requires-Dist: tiktoken
|
15
16
|
Requires-Dist: types-beautifulsoup4>=4.12.0.20240511
|
16
|
-
Requires-Dist: types-colorama>=0.4.15.20240311
|
17
17
|
Requires-Dist: types-requests>=2.32.0.20240712
|
18
18
|
Requires-Dist: yfinance
|
19
19
|
Description-Content-Type: text/markdown
|
@@ -0,0 +1,17 @@
|
|
1
|
+
atcdr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
atcdr/download.py,sha256=aqJEmrLop_Mj8GNJjoWRcSzWU0z9iNc9vtZOjhWXx3U,8945
|
3
|
+
atcdr/generate.py,sha256=0yWX-5PS-FR6LTaP3muHq6a7rFB2a1Oek48mF45exoA,6972
|
4
|
+
atcdr/main.py,sha256=y2IkXwcAyKZ_1y5PgU93GpXzo5lKak9oxo0XV_9d5Fo,727
|
5
|
+
atcdr/markdown.py,sha256=jEktnYgrDYcgIuhxRpJImAzNpFmfSPkRikAesfMxAVk,1125
|
6
|
+
atcdr/open.py,sha256=2UlmNWdieoMrPu1xSUWf-8sBB9Y19r0t6V9zDRBSPes,924
|
7
|
+
atcdr/test.py,sha256=hAhttwVJiDJX8IAWcnpKj04yTTs4cmr8GQ-NsldBAGc,8468
|
8
|
+
atcdr/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
|
+
atcdr/util/cost.py,sha256=0c9H8zLley7xZDLuYU4zJmB8m71qcO1WEIQOoEavD_4,3168
|
10
|
+
atcdr/util/execute.py,sha256=tcYflnVo_38LdaOGDUAuqfSfcA54bTrCaTRShH7kwUw,1750
|
11
|
+
atcdr/util/filetype.py,sha256=NyTkBbL44VbPwGXps381odbC_JEx_eYxRYPaYwRHfZ0,1647
|
12
|
+
atcdr/util/gpt.py,sha256=Lto6SJHZGer8cC_Nq8lJVnaET2R7apFQteo6ZEFpjdM,3304
|
13
|
+
atcdr/util/problem.py,sha256=WprmpOZm6xpyvksIS3ou1uHqFnBO1FUZWadsLziG1bY,2484
|
14
|
+
atcoderstudybooster-0.21.dist-info/METADATA,sha256=bbRsuYSoizj_QSRzfxzxF0tsMNtMqSdze9mrEph5ZHk,4468
|
15
|
+
atcoderstudybooster-0.21.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
16
|
+
atcoderstudybooster-0.21.dist-info/entry_points.txt,sha256=_bhz0R7vp2VubKl_eIokDO8Wz9TdqvYA7Q59uWfy6Sk,42
|
17
|
+
atcoderstudybooster-0.21.dist-info/RECORD,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
atcdr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
atcdr/download.py,sha256=UYOkKoFaN7Rj2p-13MOjvgq26QzdvoNmWKBaUW6cCKs,7995
|
3
|
-
atcdr/generate.py,sha256=Bh0QHRUVeI8u5FvXbJssbs6gr55XtUkNT2p897FlUgs,5521
|
4
|
-
atcdr/main.py,sha256=i-TFxFk7bFMtKZxtDgI7aPoZAF-dsXqNoz3O_ZsGvb4,605
|
5
|
-
atcdr/open.py,sha256=vbOy3fthklhZ7_WIWNGyS2H3iK2FHLeClDt_tloJ_b0,924
|
6
|
-
atcdr/test.py,sha256=it3QjFxdlR0GY6Hc0c2Qdke71Z4dj5eLhfuWVZU9cZA,7969
|
7
|
-
atcdr/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
atcdr/util/cost.py,sha256=0c9H8zLley7xZDLuYU4zJmB8m71qcO1WEIQOoEavD_4,3168
|
9
|
-
atcdr/util/filename.py,sha256=taCgSwIpB5iCjWZrYWAGRncRyUUl9exoNfsP-KLF2bs,2984
|
10
|
-
atcdr/util/gpt.py,sha256=Lto6SJHZGer8cC_Nq8lJVnaET2R7apFQteo6ZEFpjdM,3304
|
11
|
-
atcdr/util/problem.py,sha256=iDfNGfoCk_sy9RQRZ4vVqd1ViyT8HSWe_ekKUb4PdKs,2412
|
12
|
-
atcoderstudybooster-0.1.1.dist-info/METADATA,sha256=6ncUVzTFelY1WHUoNP4NNLIqsjHiDAh_54Lgsg3OruI,4478
|
13
|
-
atcoderstudybooster-0.1.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
14
|
-
atcoderstudybooster-0.1.1.dist-info/entry_points.txt,sha256=_bhz0R7vp2VubKl_eIokDO8Wz9TdqvYA7Q59uWfy6Sk,42
|
15
|
-
atcoderstudybooster-0.1.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|