AtCoderStudyBooster 0.1.1__tar.gz → 0.21__tar.gz

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.
Files changed (29) hide show
  1. atcoderstudybooster-0.21/.github/workflows/deploy.yaml +40 -0
  2. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/PKG-INFO +3 -3
  3. atcoderstudybooster-0.21/atcdr/download.py +289 -0
  4. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/atcdr/generate.py +76 -23
  5. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/atcdr/main.py +5 -0
  6. atcoderstudybooster-0.21/atcdr/markdown.py +39 -0
  7. atcoderstudybooster-0.21/atcdr/open.py +43 -0
  8. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/atcdr/test.py +61 -43
  9. atcoderstudybooster-0.21/atcdr/util/execute.py +63 -0
  10. atcoderstudybooster-0.1.1/atcdr/util/filename.py → atcoderstudybooster-0.21/atcdr/util/filetype.py +2 -48
  11. atcoderstudybooster-0.21/atcdr/util/problem.py +91 -0
  12. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/pyproject.toml +3 -3
  13. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/requirements-dev.lock +14 -4
  14. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/requirements.lock +14 -4
  15. atcoderstudybooster-0.1.1/atcdr/download.py +0 -266
  16. atcoderstudybooster-0.1.1/atcdr/open.py +0 -36
  17. atcoderstudybooster-0.1.1/atcdr/util/problem.py +0 -87
  18. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/.gitignore +0 -0
  19. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/.images/demo1.png +0 -0
  20. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/.pre-commit-config.yaml +0 -0
  21. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/.python-version +0 -0
  22. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/.vscode/extensions.json +0 -0
  23. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/.vscode/setting.json +0 -0
  24. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/.vscode/tasks.json +0 -0
  25. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/README.md +0 -0
  26. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/atcdr/__init__.py +0 -0
  27. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/atcdr/util/__init__.py +0 -0
  28. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/atcdr/util/cost.py +0 -0
  29. {atcoderstudybooster-0.1.1 → atcoderstudybooster-0.21}/atcdr/util/gpt.py +0 -0
@@ -0,0 +1,40 @@
1
+ name: Deploy to PyPI
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+ branches:
7
+ - main
8
+ release:
9
+ types: [published]
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ deploy:
16
+ if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.ref == 'develop'
17
+ name: Deploy
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - name: Checkout code
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Install Rye
25
+ shell: bash
26
+ run: |
27
+ curl -sSf https://rye.astral.sh/get | bash
28
+ echo 'source "$HOME/.rye/env"' >> ~/.bashrc
29
+ source ~/.bashrc
30
+
31
+ - name: Sync dependencies
32
+ run: rye sync
33
+
34
+ - name: Build
35
+ run: rye build
36
+
37
+ - name: Publish to PyPI
38
+ env:
39
+ PYPI_TOKEN: ${{ secrets.PYPI }}
40
+ run: rye publish --token $PYPI_TOKEN --yes
@@ -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,289 @@
1
+ import os
2
+ import re
3
+ import time
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from typing import Callable, List, Optional, Union, cast
7
+
8
+ import questionary as q
9
+ import requests
10
+ from rich.console import Console
11
+ from rich.prompt import IntPrompt, Prompt
12
+
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()
22
+
23
+
24
+ class Diff(Enum):
25
+ A = 'A'
26
+ B = 'B'
27
+ C = 'C'
28
+ D = 'D'
29
+ E = 'E'
30
+ F = 'F'
31
+ G = 'G'
32
+
33
+
34
+ @dataclass
35
+ class Problem:
36
+ number: int
37
+ difficulty: Diff
38
+
39
+
40
+ def get_problem_html(problem: Problem) -> Optional[str]:
41
+ url = f'https://atcoder.jp/contests/abc{problem.number}/tasks/abc{problem.number}_{problem.difficulty.value.lower()}'
42
+ response = requests.get(url)
43
+ retry_attempts = 3
44
+ retry_wait = 1 # 1 second
45
+
46
+ for _ in range(retry_attempts):
47
+ response = requests.get(url)
48
+ if response.status_code == 200:
49
+ return response.text
50
+ elif response.status_code == 429:
51
+ console.print(
52
+ f'[bold yellow][Error {response.status_code}][/bold yellow] 再試行します。abc{problem.number} {problem.difficulty.value}'
53
+ )
54
+ time.sleep(retry_wait)
55
+ elif 300 <= response.status_code < 400:
56
+ console.print(
57
+ f'[bold yellow][Error {response.status_code}][/bold yellow] リダイレクトが発生しました。abc{problem.number} {problem.difficulty.value}'
58
+ )
59
+ elif 400 <= response.status_code < 500:
60
+ console.print(
61
+ f'[bold red][Error {response.status_code}][/bold red] 問題が見つかりません。abc{problem.number} {problem.difficulty.value}'
62
+ )
63
+ break
64
+ elif 500 <= response.status_code < 600:
65
+ console.print(
66
+ f'[bold red][Error {response.status_code}][/bold red] サーバーエラーが発生しました。abc{problem.number} {problem.difficulty.value}'
67
+ )
68
+ break
69
+ else:
70
+ console.print(
71
+ f'[bold red][Error {response.status_code}][/bold red] abc{problem.number} {problem.difficulty.value}に対応するHTMLファイルを取得できませんでした。'
72
+ )
73
+ break
74
+ return None
75
+
76
+
77
+ def save_file(file_path: str, html: str) -> None:
78
+ with open(file_path, 'w', encoding='utf-8') as file:
79
+ file.write(html)
80
+ console.print(f'[bold green][+][/bold green] ファイルを保存しました :{file_path}')
81
+
82
+
83
+ def mkdir(path: str) -> None:
84
+ if not os.path.exists(path):
85
+ os.makedirs(path)
86
+ console.print(f'[bold green][+][/bold green] フォルダー: {path} を作成しました')
87
+
88
+
89
+ class GenerateMode:
90
+ @staticmethod
91
+ def gene_path_on_diff(base: str, number: int, diff: Diff) -> str:
92
+ return os.path.join(base, diff.name, str(number))
93
+
94
+ @staticmethod
95
+ def gene_path_on_num(base: str, number: int, diff: Diff) -> str:
96
+ return os.path.join(base, str(number), diff.name)
97
+
98
+
99
+ def generate_problem_directory(
100
+ base_path: str, problems: List[Problem], gene_path: Callable[[str, int, Diff], str]
101
+ ) -> None:
102
+ for problem in problems:
103
+ dir_path = gene_path(base_path, problem.number, problem.difficulty)
104
+
105
+ html = get_problem_html(problem)
106
+ if html is None:
107
+ continue
108
+
109
+ title = get_title_from_html(html)
110
+ if not title:
111
+ console.print('[bold red][Error][/bold red] タイトルが取得できませんでした')
112
+ title = f'problem{problem.number}{problem.difficulty.value}'
113
+
114
+ title = title_to_filename(title)
115
+
116
+ mkdir(dir_path)
117
+ repaired_html = repair_html(html)
118
+
119
+ html_path = os.path.join(dir_path, title + FILE_EXTENSIONS[Lang.HTML])
120
+ save_file(html_path, repaired_html)
121
+ md = make_problem_markdown(html, 'ja')
122
+ md_path = os.path.join(dir_path, title + FILE_EXTENSIONS[Lang.MARKDOWN])
123
+ save_file(md_path, md)
124
+
125
+
126
+ def parse_range(range_str: str) -> List[int]:
127
+ match = re.match(r'^(\d+)\.\.(\d+)$', range_str)
128
+ if match:
129
+ start, end = map(int, match.groups())
130
+ return list(range(start, end + 1))
131
+ else:
132
+ raise ValueError('数字の範囲の形式が間違っています')
133
+
134
+
135
+ def parse_diff_range(range_str: str) -> List[Diff]:
136
+ match = re.match(r'^([A-Z])\.\.([A-Z])$', range_str)
137
+ if match:
138
+ start, end = match.groups()
139
+ start_index = ord(start) - ord('A')
140
+ end_index = ord(end) - ord('A')
141
+ if start_index <= end_index:
142
+ return [Diff(chr(i + ord('A'))) for i in range(start_index, end_index + 1)]
143
+ raise ValueError('A..C の形式になっていません')
144
+
145
+
146
+ def convert_arg(arg: str) -> Union[List[int], List[Diff]]:
147
+ if isinstance(arg, int):
148
+ return [arg]
149
+ elif isinstance(arg, str):
150
+ if arg.isdigit():
151
+ return [int(arg)]
152
+ elif arg in Diff.__members__:
153
+ return [Diff[arg]]
154
+ elif re.match(r'^\d+\.\.\d+$', arg):
155
+ return parse_range(arg)
156
+ elif re.match(r'^[A-Z]\.\.[A-Z]$', arg):
157
+ return parse_diff_range(arg)
158
+ raise ValueError(f'{arg}は認識できません')
159
+
160
+
161
+ def are_all_integers(args: Union[List[int], List[Diff]]) -> bool:
162
+ return all(isinstance(arg, int) for arg in args)
163
+
164
+
165
+ def are_all_diffs(args: Union[List[int], List[Diff]]) -> bool:
166
+ return all(isinstance(arg, Diff) for arg in args)
167
+
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
+
243
+ def download(
244
+ first: Union[str, int, None] = None,
245
+ second: Union[str, int, None] = None,
246
+ base_path: str = '.',
247
+ ) -> None:
248
+ if first is None:
249
+ interactive_download()
250
+ return
251
+
252
+ first_args = convert_arg(str(first))
253
+ if second is None:
254
+ if isinstance(first, Diff):
255
+ raise ValueError(
256
+ """難易度だけでなく, 問題番号も指定してコマンドを実行してください.
257
+ 例 atcdr -d A 120 : A問題の120をダウンロードます
258
+ 例 atcdr -d A 120..130 : A問題の120から130をダウンロードます
259
+ """
260
+ )
261
+ second_args: Union[List[int], List[Diff]] = list(Diff)
262
+ else:
263
+ second_args = convert_arg(str(second))
264
+
265
+ if are_all_integers(first_args) and are_all_diffs(second_args):
266
+ first_args_int = cast(List[int], first_args)
267
+ second_args_diff = cast(List[Diff], second_args)
268
+ problems = [
269
+ Problem(number, diff)
270
+ for number in first_args_int
271
+ for diff in second_args_diff
272
+ ]
273
+ generate_problem_directory(base_path, problems, GenerateMode.gene_path_on_num)
274
+ elif are_all_diffs(first_args) and are_all_integers(second_args):
275
+ first_args_diff = cast(List[Diff], first_args)
276
+ second_args_int = cast(List[int], second_args)
277
+ problems = [
278
+ Problem(number, diff)
279
+ for diff in first_args_diff
280
+ for number in second_args_int
281
+ ]
282
+ generate_problem_directory(base_path, problems, GenerateMode.gene_path_on_diff)
283
+ else:
284
+ raise ValueError(
285
+ """次のような形式で問題を指定してください
286
+ 例 atcdr -d A 120..130 : A問題の120から130をダウンロードします
287
+ 例 atcdr -d 120 : ABCのコンテストの問題をダウンロードします
288
+ """
289
+ )
@@ -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
 
@@ -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
 
@@ -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])
@@ -0,0 +1,43 @@
1
+ import webbrowser # noqa: I001
2
+ from rich.panel import Panel
3
+ from rich.console import Console
4
+
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
8
+
9
+
10
+ def open_html(file: str) -> None:
11
+ console = Console()
12
+ try:
13
+ with open(file, 'r') as f:
14
+ html_content = f.read()
15
+ except FileNotFoundError:
16
+ console.print(
17
+ Panel(
18
+ f"{file}' [red]が見つかりません[/]",
19
+ border_style='red',
20
+ )
21
+ )
22
+ return
23
+
24
+ url = find_link_from_html(html_content)
25
+ if url:
26
+ webbrowser.open_new_tab(url)
27
+ console.print(
28
+ Panel(
29
+ f'[green]URLを開きました[/] {url}',
30
+ border_style='green',
31
+ )
32
+ )
33
+ else:
34
+ console.print(
35
+ Panel(
36
+ f'{file} [yellow]にURLが見つかりませんでした[/]',
37
+ border_style='yellow',
38
+ )
39
+ )
40
+
41
+
42
+ def open_files(*args: str) -> None:
43
+ execute_files(*args, func=open_html, target_filetypes=[Lang.HTML])