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 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, Match, Optional, Union, cast
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.problem import make_problem_markdown
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}] 再試行します. abc{problem.number} {problem.difficulty.value}'
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'[Erroe{response.status_code}] リダイレクトが発生しました。abc{problem.number} {problem.difficulty.value}'
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 is None:
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, f'{title}.html')
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
- save_file(os.path.join(dir_path, f'{title}.md'), md)
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('Invalid range format')
132
+ raise ValueError('数字の範囲の形式が間違っています')
140
133
 
141
134
 
142
135
  def parse_diff_range(range_str: str) -> List[Diff]:
143
- match = re.match(r'^([A-F])\.\.([A-F])$', range_str)
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: Union[str, int]) -> Union[List[int], List[Diff]]:
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-F]\.\.[A-F]$', arg):
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
- main()
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
- 例 atcdr -d A 120 : A問題の120をダウンロードます
191
- 例 atcdr -d A 120..130 : A問題の120から130をダウンロードます
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
- 例 atcdr -d A 120..130 : A問題の120から130をダウンロードします
220
- 例 atcdr -d 120 : ABCのコンテストの問題をダウンロードします
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
- render_result,
12
+ render_results,
10
13
  )
11
- from atcdr.util.filename import (
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(f'[+]:{gpt.model.value}の出力したコードを保存しました:{f.name}')
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
- reply = gpt.tell(md + propmpt)
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, 'w') as f:
79
- print(f'[+]:テンプレートファイル{savaed_filename}を作成しました.')
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(f'[+]:{gpt.model.value}の出力したコードを保存しました:{f.name}')
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(render_result(lresult) for lresult in labeled_results)
166
+ test_report = '\n'.join(
167
+ render_result_for_GPT(lresult) for lresult in labeled_results
168
+ )
114
169
 
115
- print(f'{i}回目のコード生成でのテスト結果:---')
116
- print(test_report)
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(f'[+]:{gpt.model.value}の出力のログを保存しました:{f.name}')
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 bs4 import BeautifulSoup as bs
4
- from bs4.element import Tag
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(f"HTMLファイル '{file}' が見つかりません。")
16
+ console.print(
17
+ Panel(
18
+ f"{file}' [red]が見つかりません[/]",
19
+ border_style='red',
20
+ )
21
+ )
26
22
  return
27
23
 
28
- url = find_link_from(html_content)
24
+ url = find_link_from_html(html_content)
29
25
  if url:
30
- webbrowser.open(url)
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('URLが見つかりませんでした。')
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 colorama import Fore
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.filename import FILE_EXTENSIONS, SOURCE_LANGUAGES, Lang, execute_files
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
- CHECK_MARK = '\u2713'
225
- CROSS_MARK = '\u00d7'
226
-
227
-
228
- def render_result(lresult: LabeledTestCaseResult) -> str:
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
- return output
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
- output = '\n'.join(render_result(lresult) for lresult in test_results)
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 Callable, Dict, List, TypeAlias
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 Optional, Union
3
+ from typing import Match, Optional
4
4
 
5
5
  from bs4 import BeautifulSoup as bs
6
- from bs4 import NavigableString, Tag
6
+ from bs4 import Tag
7
7
  from markdownify import MarkdownConverter # type: ignore
8
8
 
9
9
 
10
- # TODO : そのうちgenerate.pyやtest.py, open.pyのHTMLのparse処理を全部まとめる
11
- class Lang(Enum):
12
- JA = 'ja'
13
- EN = 'en'
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
- class ProblemStruct:
17
- def __init__(self) -> None:
18
- self.problem_part: Optional[str] = None
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
- parts = task_statement.find_all('div', {'class': 'part'})
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
- io_div = task_statement.find('div', {'class': 'io-style'})
34
- if isinstance(io_div, Tag):
35
- io_parts = io_div.find_all('div', {'class': 'part'})
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
- # 2つ目以降のdivをtest_partに格納
43
- self.test_part = [str(part) for part in io_parts[1:]]
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(md_text):
61
- md_text = re.sub(r'\n\s*\n\s*\n+', '\n\n', md_text)
62
- md_text = md_text.strip()
63
- return md_text
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.1.1
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,,