AtCoderStudyBooster 0.1.0__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.
@@ -0,0 +1,48 @@
1
+ # Pythonのバイトコードファイル
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.py[oc]
5
+
6
+ # ビルドディレクトリ
7
+ build/
8
+ dist/
9
+ wheels/
10
+ *.egg-info/
11
+
12
+ # Python仮想環境
13
+ .venv
14
+ venv/
15
+ env/
16
+ *.venv/
17
+
18
+ # コンパイルされたファイル
19
+ *.so
20
+ *.dylib
21
+ *.dll
22
+
23
+ # Jupyter Notebook
24
+ .ipynb_checkpoints/
25
+
26
+ # 環境依存のファイル
27
+ *.env
28
+ .env
29
+
30
+ # IDE/エディタの設定ファイル
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+
35
+ # 一時ファイル
36
+ *.log
37
+ *.tmp
38
+ *.bak
39
+
40
+ # デバッグファイル
41
+ *.pdb
42
+
43
+ # OS固有の隠しファイル
44
+ .DS_Store
45
+ Thumbs.db
46
+
47
+ # キャッシュファイル
48
+ .*_cache
@@ -0,0 +1,23 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.6.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+
8
+ - repo: https://github.com/astral-sh/ruff-pre-commit
9
+ # Ruff version.
10
+ rev: v0.5.7
11
+ hooks:
12
+ # Run the linter.
13
+ - id: ruff
14
+ args: [--fix]
15
+ # Run the formatter.
16
+ - id: ruff-format
17
+
18
+ - repo: https://github.com/pre-commit/mirrors-mypy
19
+ rev: v1.11.1
20
+ hooks:
21
+ - id: mypy
22
+ additional_dependencies: [types-requests]
23
+ args: [--ignore-missing-imports]
@@ -0,0 +1 @@
1
+ 3.12.4
@@ -0,0 +1,5 @@
1
+ {
2
+ "recommendations": [
3
+ "charliermarsh.ruff",
4
+ ]
5
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "[python]": {
3
+ "editor.formatOnSave": true,
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll": true,
6
+ "source.organizeImports": true
7
+ },
8
+ "editor.defaultFormatter": "charliermarsh.ruff"
9
+ }
10
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "Run Ruff",
6
+ "type": "shell",
7
+ "command": "ruff",
8
+ "args": [
9
+ "."
10
+ ],
11
+ "group": {
12
+ "kind": "build",
13
+ "isDefault": true
14
+ },
15
+ "problemMatcher": []
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.3
2
+ Name: AtCoderStudyBooster
3
+ Version: 0.1.0
4
+ Summary: A tool to download and manage AtCoder problems.
5
+ Author-email: yuta6 <46110512+yuta6@users.noreply.github.com>
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: beautifulsoup4
8
+ Requires-Dist: colorama
9
+ Requires-Dist: fire
10
+ Requires-Dist: markdownify>=0.13.1
11
+ Requires-Dist: requests
12
+ Requires-Dist: tiktoken
13
+ Requires-Dist: types-beautifulsoup4>=4.12.0.20240511
14
+ Requires-Dist: types-colorama>=0.4.15.20240311
15
+ Requires-Dist: types-requests>=2.32.0.20240712
16
+ Requires-Dist: yfinance
17
+ Description-Content-Type: text/markdown
18
+
19
+ # AtCoderStudyBooster
20
+
21
+ ## 概要
22
+
23
+ AtCoderStudyBoosterはAtCoderの学習を加速させるためのツールです。問題をローカルにダウンロードし、テスト、解答の作成をサポートするツールです。Pythonが入っていることが必須です。Pythonが入っている環境なら、`pip install AtCoderStudyBooster`でインストールできます。
24
+
25
+ このツールは以下のプロジェクトに強く影響を受けています。
26
+ [online-judge-tools](https://github.com/online-judge-tools)
27
+ [atcoder-cli](https://github.com/Tatamo/atcoder-cli)
28
+ これらとの違いですが、本ツールはAtCoderでのコンテストでの利用は想定しておらず、初心者の学習のサポートのみを意識しています。そのため、現時点で提出機能は備えていません。また, Chat GPT APIによる解答の作成サポート機能を備えています。
29
+
30
+ ## 利用ケース
31
+
32
+ ### B問題の練習したい場合
33
+
34
+ ABCコンテストの223から226のB問題だけを集中的に練習したい場合、次のコマンドを実行します。
35
+
36
+ ```sh
37
+ atcdr download B 223..226
38
+ ```
39
+
40
+ コマンドを実行すると,次のようなフォルダーを作成して、各々のフォルダーに問題をダウンロードします。
41
+
42
+ ```css
43
+ B
44
+ ├── 223
45
+ │ ├── StringShifting.html
46
+ │ └── StringShifting.md
47
+ ├── 224
48
+ │ ├── Mongeness.html
49
+ │ └── Mongeness.md
50
+ ├── 225
51
+ │ ├── StarorNot.html
52
+ │ └── StarorNot.md
53
+ └── 226
54
+ ├── CountingArrays.html
55
+ └── CountingArrays.md
56
+ ```
57
+
58
+ MarkdownファイルあるいはHTMLファイルをVS CodeのHTML Preview, Markdown Previewで開くと問題を確認できます。VS Codeで開くと左側にテキストエディターを表示して、右側で問題をみながら問題に取り組めます。
59
+
60
+ ![demo画像](./.images/demo1.png)
61
+
62
+ ### サンプルをローカルでテストする
63
+
64
+ 問題をダウンロードしたフォルダーに移動します。
65
+
66
+ ```sh
67
+ cd B/224
68
+ ```
69
+
70
+ 移動したフォルダーで解答ファイルを作成後を実行すると, 自動的にテストします。
71
+
72
+ ```sh
73
+ ▷ ~/.../B/224
74
+ atcdr t
75
+ ```
76
+
77
+ ```sh
78
+ solution.pyをテストします。
79
+ --------------------
80
+
81
+ Sample 1 of Test:
82
+ ✓ Accepted !! Time: 24 ms
83
+
84
+ Sample 2 of Test:
85
+ ✓ Accepted !! Time: 15 ms
86
+ ```
87
+
88
+ と実行すると作成したソースコードをテストして、HTMLに書かれているテストケースを読み込んで実行し, Passするかを判定します。
89
+
90
+ ## 解答生成機能generateコマンドに関する注意点
91
+
92
+ 本ツールにはChatGPT APIを利用したコード生成機能があります。[AtCoder生成AI対策ルール](https://info.atcoder.jp/entry/llm-abc-rules-ja?_gl=1*1axgs02*_ga*ODc0NDAyNjA4LjE3MTk1ODEyNDA.*_ga_RC512FD18N*MTcyMzMxNDA1Ni43NC4xLjE3MjMzMTY1NjUuMC4wLjA.)によるとAtCoder Beginner Contestにおいてに問題文を生成AIに直接与えることは禁止されています。ただし、このルールは過去問を練習している際には適用されません。
93
+
94
+ 現時点で本ツールにはログイン機能がないため、コンテスト中の問題に対して`download`コマンドは利用して問題をダウンロードすることはできません。`generate`コマンドは`download`コマンドに依存しており、ダウンロードした問題のHTMLファイルをパースしてGPTに解釈しやすいmarkdownを与えることで実現しています。したがって、このコマンドがAtCoder Beginner Contest中に[AtCoder生成AI対策ルール](https://info.atcoder.jp/entry/llm-abc-rules-ja?_gl=1*1axgs02*_ga*ODc0NDAyNjA4LjE3MTk1ODEyNDA.*_ga_RC512FD18N*MTcyMzMxNDA1Ni43NC4xLjE3MjMzMTY1NjUuMC4wLjA.)に抵触することはありません。
@@ -0,0 +1,76 @@
1
+ # AtCoderStudyBooster
2
+
3
+ ## 概要
4
+
5
+ AtCoderStudyBoosterはAtCoderの学習を加速させるためのツールです。問題をローカルにダウンロードし、テスト、解答の作成をサポートするツールです。Pythonが入っていることが必須です。Pythonが入っている環境なら、`pip install AtCoderStudyBooster`でインストールできます。
6
+
7
+ このツールは以下のプロジェクトに強く影響を受けています。
8
+ [online-judge-tools](https://github.com/online-judge-tools)
9
+ [atcoder-cli](https://github.com/Tatamo/atcoder-cli)
10
+ これらとの違いですが、本ツールはAtCoderでのコンテストでの利用は想定しておらず、初心者の学習のサポートのみを意識しています。そのため、現時点で提出機能は備えていません。また, Chat GPT APIによる解答の作成サポート機能を備えています。
11
+
12
+ ## 利用ケース
13
+
14
+ ### B問題の練習したい場合
15
+
16
+ ABCコンテストの223から226のB問題だけを集中的に練習したい場合、次のコマンドを実行します。
17
+
18
+ ```sh
19
+ atcdr download B 223..226
20
+ ```
21
+
22
+ コマンドを実行すると,次のようなフォルダーを作成して、各々のフォルダーに問題をダウンロードします。
23
+
24
+ ```css
25
+ B
26
+ ├── 223
27
+ │ ├── StringShifting.html
28
+ │ └── StringShifting.md
29
+ ├── 224
30
+ │ ├── Mongeness.html
31
+ │ └── Mongeness.md
32
+ ├── 225
33
+ │ ├── StarorNot.html
34
+ │ └── StarorNot.md
35
+ └── 226
36
+ ├── CountingArrays.html
37
+ └── CountingArrays.md
38
+ ```
39
+
40
+ MarkdownファイルあるいはHTMLファイルをVS CodeのHTML Preview, Markdown Previewで開くと問題を確認できます。VS Codeで開くと左側にテキストエディターを表示して、右側で問題をみながら問題に取り組めます。
41
+
42
+ ![demo画像](./.images/demo1.png)
43
+
44
+ ### サンプルをローカルでテストする
45
+
46
+ 問題をダウンロードしたフォルダーに移動します。
47
+
48
+ ```sh
49
+ cd B/224
50
+ ```
51
+
52
+ 移動したフォルダーで解答ファイルを作成後を実行すると, 自動的にテストします。
53
+
54
+ ```sh
55
+ ▷ ~/.../B/224
56
+ atcdr t
57
+ ```
58
+
59
+ ```sh
60
+ solution.pyをテストします。
61
+ --------------------
62
+
63
+ Sample 1 of Test:
64
+ ✓ Accepted !! Time: 24 ms
65
+
66
+ Sample 2 of Test:
67
+ ✓ Accepted !! Time: 15 ms
68
+ ```
69
+
70
+ と実行すると作成したソースコードをテストして、HTMLに書かれているテストケースを読み込んで実行し, Passするかを判定します。
71
+
72
+ ## 解答生成機能generateコマンドに関する注意点
73
+
74
+ 本ツールにはChatGPT APIを利用したコード生成機能があります。[AtCoder生成AI対策ルール](https://info.atcoder.jp/entry/llm-abc-rules-ja?_gl=1*1axgs02*_ga*ODc0NDAyNjA4LjE3MTk1ODEyNDA.*_ga_RC512FD18N*MTcyMzMxNDA1Ni43NC4xLjE3MjMzMTY1NjUuMC4wLjA.)によるとAtCoder Beginner Contestにおいてに問題文を生成AIに直接与えることは禁止されています。ただし、このルールは過去問を練習している際には適用されません。
75
+
76
+ 現時点で本ツールにはログイン機能がないため、コンテスト中の問題に対して`download`コマンドは利用して問題をダウンロードすることはできません。`generate`コマンドは`download`コマンドに依存しており、ダウンロードした問題のHTMLファイルをパースしてGPTに解釈しやすいmarkdownを与えることで実現しています。したがって、このコマンドがAtCoder Beginner Contest中に[AtCoder生成AI対策ルール](https://info.atcoder.jp/entry/llm-abc-rules-ja?_gl=1*1axgs02*_ga*ODc0NDAyNjA4LjE3MTk1ODEyNDA.*_ga_RC512FD18N*MTcyMzMxNDA1Ni43NC4xLjE3MjMzMTY1NjUuMC4wLjA.)に抵触することはありません。
File without changes
@@ -0,0 +1,266 @@
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, Match, Optional, Union, cast
7
+
8
+ import requests
9
+
10
+ from atcdr.util.problem import make_problem_markdown
11
+
12
+
13
+ class Diff(Enum):
14
+ A = 'A'
15
+ B = 'B'
16
+ C = 'C'
17
+ D = 'D'
18
+ E = 'E'
19
+ F = 'F'
20
+ G = 'G'
21
+
22
+
23
+ @dataclass
24
+ class Problem:
25
+ number: int
26
+ difficulty: Diff
27
+
28
+
29
+ def get_problem_html(problem: Problem) -> Optional[str]:
30
+ url = f'https://atcoder.jp/contests/abc{problem.number}/tasks/abc{problem.number}_{problem.difficulty.value.lower()}'
31
+ response = requests.get(url)
32
+ retry_attempts = 3
33
+ retry_wait = 1 # 1 second
34
+
35
+ for _ in range(retry_attempts):
36
+ response = requests.get(url)
37
+ if response.status_code == 200:
38
+ return response.text
39
+ elif response.status_code == 429:
40
+ print(
41
+ f'[Error{response.status_code}] 再試行します. abc{problem.number} {problem.difficulty.value}'
42
+ )
43
+ time.sleep(retry_wait)
44
+ elif 300 <= response.status_code < 400:
45
+ print(
46
+ f'[Erroe{response.status_code}] リダイレクトが発生しました。abc{problem.number} {problem.difficulty.value}'
47
+ )
48
+ elif 400 <= response.status_code < 500:
49
+ print(
50
+ f'[Error{response.status_code}] 問題が見つかりません。abc{problem.number} {problem.difficulty.value}'
51
+ )
52
+ break
53
+ elif 500 <= response.status_code < 600:
54
+ print(
55
+ f'[Error{response.status_code}] サーバーエラーが発生しました。abc{problem.number} {problem.difficulty.value}'
56
+ )
57
+ break
58
+ else:
59
+ print(
60
+ f'[Error{response.status_code}] abc{problem.number} {problem.difficulty.value}に対応するHTMLファイルを取得できませんでした。'
61
+ )
62
+ break
63
+ return None
64
+
65
+
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
+ def save_file(file_path: str, html: str) -> None:
88
+ with open(file_path, 'w', encoding='utf-8') as file:
89
+ file.write(html)
90
+ print(f'[+] ファイルを保存しました :{file_path}')
91
+
92
+
93
+ def mkdir(path: str) -> None:
94
+ if not os.path.exists(path):
95
+ os.makedirs(path)
96
+ print(f'[+] フォルダー: {path} を作成しました')
97
+
98
+
99
+ class GenerateMode:
100
+ @staticmethod
101
+ def gene_path_on_diff(base: str, number: int, diff: Diff) -> str:
102
+ return os.path.join(base, diff.name, str(number))
103
+
104
+ @staticmethod
105
+ def gene_path_on_num(base: str, number: int, diff: Diff) -> str:
106
+ return os.path.join(base, str(number), diff.name)
107
+
108
+
109
+ def generate_problem_directory(
110
+ base_path: str, problems: List[Problem], gene_path: Callable[[str, int, Diff], str]
111
+ ) -> None:
112
+ for problem in problems:
113
+ dir_path = gene_path(base_path, problem.number, problem.difficulty)
114
+
115
+ html = get_problem_html(problem)
116
+ if html is None:
117
+ continue
118
+
119
+ title = get_title_from_html(html)
120
+ if title is None:
121
+ print('[Error] タイトルが取得できませんでした')
122
+ title = f'problem{problem.number}{problem.difficulty.value}'
123
+
124
+ mkdir(dir_path)
125
+ repaired_html = repair_html(html)
126
+
127
+ html_path = os.path.join(dir_path, f'{title}.html')
128
+ save_file(html_path, repaired_html)
129
+ md = make_problem_markdown(html, 'ja')
130
+ save_file(os.path.join(dir_path, f'{title}.md'), md)
131
+
132
+
133
+ def parse_range(range_str: str) -> List[int]:
134
+ match = re.match(r'^(\d+)\.\.(\d+)$', range_str)
135
+ if match:
136
+ start, end = map(int, match.groups())
137
+ return list(range(start, end + 1))
138
+ else:
139
+ raise ValueError('Invalid range format')
140
+
141
+
142
+ def parse_diff_range(range_str: str) -> List[Diff]:
143
+ match = re.match(r'^([A-F])\.\.([A-F])$', range_str)
144
+ if match:
145
+ start, end = match.groups()
146
+ start_index = ord(start) - ord('A')
147
+ end_index = ord(end) - ord('A')
148
+ if start_index <= end_index:
149
+ return [Diff(chr(i + ord('A'))) for i in range(start_index, end_index + 1)]
150
+ raise ValueError('A..C の形式になっていません')
151
+
152
+
153
+ def convert_arg(arg: Union[str, int]) -> Union[List[int], List[Diff]]:
154
+ if isinstance(arg, int):
155
+ return [arg]
156
+ elif isinstance(arg, str):
157
+ if arg.isdigit():
158
+ return [int(arg)]
159
+ elif arg in Diff.__members__:
160
+ return [Diff[arg]]
161
+ elif re.match(r'^\d+\.\.\d+$', arg):
162
+ return parse_range(arg)
163
+ elif re.match(r'^[A-F]\.\.[A-F]$', arg):
164
+ return parse_diff_range(arg)
165
+ raise ValueError(f'{arg}は認識できません')
166
+
167
+
168
+ def are_all_integers(args: Union[List[int], List[Diff]]) -> bool:
169
+ return all(isinstance(arg, int) for arg in args)
170
+
171
+
172
+ def are_all_diffs(args: Union[List[int], List[Diff]]) -> bool:
173
+ return all(isinstance(arg, Diff) for arg in args)
174
+
175
+
176
+ def download(
177
+ first: Union[str, int, None] = None,
178
+ second: Union[str, int, None] = None,
179
+ base_path: str = '.',
180
+ ) -> None:
181
+ if first is None:
182
+ main()
183
+ return
184
+
185
+ first_args = convert_arg(str(first))
186
+ if second is None:
187
+ if isinstance(first, Diff):
188
+ raise ValueError(
189
+ """難易度だけでなく, 問題番号も指定してコマンドを実行してください.
190
+ 例 atcdr -d A 120 : A問題の120をダウンロードます
191
+ 例 atcdr -d A 120..130 : A問題の120から130をダウンロードます
192
+ """
193
+ )
194
+ second_args: Union[List[int], List[Diff]] = list(Diff)
195
+ else:
196
+ second_args = convert_arg(str(second))
197
+
198
+ if are_all_integers(first_args) and are_all_diffs(second_args):
199
+ first_args_int = cast(List[int], first_args)
200
+ second_args_diff = cast(List[Diff], second_args)
201
+ problems = [
202
+ Problem(number, diff)
203
+ for number in first_args_int
204
+ for diff in second_args_diff
205
+ ]
206
+ generate_problem_directory(base_path, problems, GenerateMode.gene_path_on_num)
207
+ elif are_all_diffs(first_args) and are_all_integers(second_args):
208
+ first_args_diff = cast(List[Diff], first_args)
209
+ second_args_int = cast(List[int], second_args)
210
+ problems = [
211
+ Problem(number, diff)
212
+ for diff in first_args_diff
213
+ for number in second_args_int
214
+ ]
215
+ generate_problem_directory(base_path, problems, GenerateMode.gene_path_on_diff)
216
+ else:
217
+ raise ValueError(
218
+ """次のような形式で問題を指定してください
219
+ 例 atcdr -d A 120..130 : A問題の120から130をダウンロードします
220
+ 例 atcdr -d 120 : ABCのコンテストの問題をダウンロードします
221
+ """
222
+ )
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()