AtCoderStudyBooster 0.3.2__py3-none-any.whl → 0.4.0__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 +123 -223
- atcdr/util/problem.py +27 -74
- {atcoderstudybooster-0.3.2.dist-info → atcoderstudybooster-0.4.0.dist-info}/METADATA +140 -92
- {atcoderstudybooster-0.3.2.dist-info → atcoderstudybooster-0.4.0.dist-info}/RECORD +6 -6
- {atcoderstudybooster-0.3.2.dist-info → atcoderstudybooster-0.4.0.dist-info}/WHEEL +0 -0
- {atcoderstudybooster-0.3.2.dist-info → atcoderstudybooster-0.4.0.dist-info}/entry_points.txt +0 -0
atcdr/download.py
CHANGED
@@ -1,22 +1,24 @@
|
|
1
|
-
import os
|
2
1
|
import re
|
3
2
|
import time
|
4
|
-
from
|
3
|
+
from itertools import groupby, product
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import List, Tuple, Union, cast
|
5
6
|
|
6
7
|
import questionary as q
|
8
|
+
import requests
|
7
9
|
import rich_click as click
|
8
10
|
from rich import print
|
9
11
|
from rich.prompt import Prompt
|
10
12
|
|
11
13
|
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
12
14
|
from atcdr.util.parse import ProblemHTML
|
13
|
-
from atcdr.util.problem import Contest,
|
15
|
+
from atcdr.util.problem import Contest, Problem
|
14
16
|
from atcdr.util.session import load_session
|
15
17
|
|
16
18
|
|
17
19
|
class Downloader:
|
18
|
-
def __init__(self) -> None:
|
19
|
-
self.session =
|
20
|
+
def __init__(self, session: requests.Session) -> None:
|
21
|
+
self.session = session
|
20
22
|
|
21
23
|
def get(self, problem: Problem) -> ProblemHTML:
|
22
24
|
session = self.session
|
@@ -54,111 +56,49 @@ class Downloader:
|
|
54
56
|
return ProblemHTML('')
|
55
57
|
|
56
58
|
|
57
|
-
def mkdir(path: str) -> None:
|
58
|
-
if not os.path.exists(path):
|
59
|
-
os.makedirs(path)
|
60
|
-
print(f'[bold green][+][/bold green] フォルダー: {path} を作成しました')
|
61
|
-
|
62
|
-
|
63
|
-
class GenerateMode:
|
64
|
-
@staticmethod
|
65
|
-
def gene_path_on_diff(base: str, problem: Problem) -> str:
|
66
|
-
return (
|
67
|
-
os.path.join(base, problem.label, f'{problem.contest.number:03}')
|
68
|
-
if problem.contest.number
|
69
|
-
else os.path.join(base, problem.label, problem.contest.contest)
|
70
|
-
)
|
71
|
-
|
72
|
-
@staticmethod
|
73
|
-
def gene_path_on_num(base: str, problem: Problem) -> str:
|
74
|
-
return (
|
75
|
-
os.path.join(base, f'{problem.contest.number:03}', problem.label)
|
76
|
-
if problem.contest.number
|
77
|
-
else os.path.join(base, problem.contest.contest, problem.label)
|
78
|
-
)
|
79
|
-
|
80
|
-
|
81
59
|
def title_to_filename(title: str) -> str:
|
82
60
|
title = re.sub(r'[\\/*?:"<>| !@#$%^&()+=\[\]{};,\']', '', title)
|
83
61
|
title = re.sub(r'.*?-', '', title)
|
84
62
|
return title
|
85
63
|
|
86
64
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
for problem in problems:
|
92
|
-
problem_content = downloader.get(problem)
|
93
|
-
if not problem_content:
|
94
|
-
print(f'[bold red][Error][/] {problem}の保存に失敗しました')
|
95
|
-
continue
|
96
|
-
|
97
|
-
dir_path = gene_path(base_path, problem)
|
98
|
-
mkdir(dir_path)
|
99
|
-
|
100
|
-
problem_content.repair_me()
|
101
|
-
|
102
|
-
title = problem_content.title or problem.label
|
103
|
-
title = title_to_filename(title)
|
104
|
-
|
105
|
-
html_path = os.path.join(dir_path, title + FILE_EXTENSIONS[Lang.HTML])
|
106
|
-
with open(html_path, 'w', encoding='utf-8') as file:
|
107
|
-
file.write(problem_content.html)
|
108
|
-
print(f'[bold green][+][/bold green] ファイルを保存しました :{html_path}')
|
109
|
-
|
110
|
-
md = problem_content.make_problem_markdown('ja')
|
111
|
-
md_path = os.path.join(dir_path, title + FILE_EXTENSIONS[Lang.MARKDOWN])
|
112
|
-
with open(md_path, 'w', encoding='utf-8') as file:
|
113
|
-
file.write(md)
|
114
|
-
print(f'[bold green][+][/bold green] ファイルを保存しました :{md_path}')
|
115
|
-
|
65
|
+
def save_problem(problem: Problem, path: Path, session: requests.Session) -> None:
|
66
|
+
"""1つの問題を保存"""
|
67
|
+
downloader = Downloader(session)
|
68
|
+
problem_content = downloader.get(problem)
|
116
69
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
return list(range(start, end + 1))
|
121
|
-
|
122
|
-
|
123
|
-
def parse_diff_range(match: re.Match) -> List[Diff]:
|
124
|
-
start, end = match.groups()
|
125
|
-
start_index = min(ord(start.upper()), ord(end.upper()))
|
126
|
-
end_index = max(ord(start.upper()), ord(end.upper()))
|
127
|
-
return [Diff(chr(i)) for i in range(start_index, end_index + 1)]
|
128
|
-
|
129
|
-
|
130
|
-
def convert_arg(arg: str) -> Union[List[int], List[Diff]]:
|
131
|
-
if arg.isdigit():
|
132
|
-
return [int(arg)]
|
133
|
-
elif arg.isalpha() and len(arg) == 1:
|
134
|
-
return [Diff(arg)]
|
135
|
-
elif match := re.match(r'^(\d+)\.\.(\d+)$', arg):
|
136
|
-
return parse_range(match)
|
137
|
-
elif match := re.match(r'^([A-Z])\.\.([A-Z])$', arg, re.IGNORECASE):
|
138
|
-
return parse_diff_range(match)
|
139
|
-
else:
|
140
|
-
raise ValueError(f'{arg}は認識できません')
|
70
|
+
if not problem_content:
|
71
|
+
print(f'[bold red][Error][/] {problem}の保存に失敗しました')
|
72
|
+
return
|
141
73
|
|
74
|
+
# ディレクトリ作成(pathをそのまま使用)
|
75
|
+
path.mkdir(parents=True, exist_ok=True)
|
142
76
|
|
143
|
-
|
144
|
-
|
77
|
+
problem_content.repair_me()
|
78
|
+
title = title_to_filename(problem_content.title or problem.label)
|
145
79
|
|
80
|
+
# HTMLファイル保存
|
81
|
+
html_path = path / (title + FILE_EXTENSIONS[Lang.HTML])
|
82
|
+
html_path.write_text(problem_content.html, encoding='utf-8')
|
83
|
+
print(f'[bold green][+][/bold green] ファイルを保存しました: {html_path}')
|
146
84
|
|
147
|
-
|
148
|
-
|
85
|
+
# Markdownファイル保存
|
86
|
+
md = problem_content.make_problem_markdown('ja')
|
87
|
+
md_path = path / (title + FILE_EXTENSIONS[Lang.MARKDOWN])
|
88
|
+
md_path.write_text(md, encoding='utf-8')
|
89
|
+
print(f'[bold green][+][/bold green] ファイルを保存しました: {md_path}')
|
149
90
|
|
150
91
|
|
151
|
-
def interactive_download() -> None:
|
92
|
+
def interactive_download(session) -> None:
|
152
93
|
CONTEST = '1. コンテストの問題を解きたい'
|
153
|
-
|
154
|
-
|
155
|
-
END = '4. 終了する'
|
94
|
+
ONE_FILE = '2. 1問だけダウンロードする'
|
95
|
+
END = '3. 終了する'
|
156
96
|
|
157
97
|
choice = q.select(
|
158
98
|
message='AtCoderの問題のHTMLファイルをダウンロードします',
|
159
99
|
qmark='',
|
160
100
|
pointer='❯❯❯',
|
161
|
-
choices=[CONTEST,
|
101
|
+
choices=[CONTEST, ONE_FILE, END],
|
162
102
|
instruction='\n 十字キーで移動,[enter]で実行',
|
163
103
|
style=q.Style(
|
164
104
|
[
|
@@ -172,70 +112,40 @@ def interactive_download() -> None:
|
|
172
112
|
).ask()
|
173
113
|
|
174
114
|
if choice == CONTEST:
|
175
|
-
name = Prompt.ask(
|
176
|
-
'コンテスト名を入力してください (例: abc012, abs, typical90)',
|
177
|
-
)
|
178
|
-
|
179
|
-
problems = Contest(name=name).problems(session=load_session())
|
180
|
-
if not problems:
|
181
|
-
print(f'[red][Error][/red] コンテスト名が間違っています: {name}')
|
182
|
-
return
|
183
|
-
|
184
|
-
generate_problem_directory('.', problems, GenerateMode.gene_path_on_num)
|
185
|
-
|
186
|
-
elif choice == PRACTICE:
|
187
|
-
difficulty = Prompt.ask(
|
188
|
-
'難易度を入力してください (例: A)',
|
189
|
-
)
|
115
|
+
name = Prompt.ask('コンテスト名を入力してください (例: abc012, abs, typical90)')
|
190
116
|
try:
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
'
|
196
|
-
)
|
197
|
-
if number_str.isdigit():
|
198
|
-
contest_numbers = [int(number_str)]
|
199
|
-
elif match := re.match(r'^\d+\.\.\d+$', number_str):
|
200
|
-
contest_numbers = parse_range(match)
|
201
|
-
else:
|
202
|
-
raise ValueError('数字の範囲の形式が間違っています')
|
203
|
-
|
204
|
-
problems = [
|
205
|
-
Problem(contest=Contest('abc', number), difficulty=difficulty)
|
206
|
-
for number in contest_numbers
|
207
|
-
]
|
208
|
-
|
209
|
-
generate_problem_directory('.', problems, GenerateMode.gene_path_on_diff)
|
117
|
+
contest = Contest(name, session)
|
118
|
+
for problem in contest.problems:
|
119
|
+
save_problem(problem, Path(contest.name) / problem.label, session)
|
120
|
+
except ValueError as e:
|
121
|
+
print(f'[red][Error][/red] {e}')
|
210
122
|
|
211
123
|
elif choice == ONE_FILE:
|
212
|
-
name = Prompt.ask(
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
generate_problem_directory('.', [problem], GenerateMode.gene_path_on_num)
|
124
|
+
name = Prompt.ask('コンテスト名を入力してください (例: abc012, abs, typical90)')
|
125
|
+
try:
|
126
|
+
contest = Contest(name, session)
|
127
|
+
problem = q.select(
|
128
|
+
message='どの問題をダウンロードしますか?',
|
129
|
+
qmark='',
|
130
|
+
pointer='❯❯❯',
|
131
|
+
choices=[
|
132
|
+
q.Choice(title=f'{p.label:10} | {p.url}', value=p)
|
133
|
+
for p in contest.problems
|
134
|
+
],
|
135
|
+
instruction='\n 十字キーで移動,[enter]で実行',
|
136
|
+
style=q.Style(
|
137
|
+
[
|
138
|
+
('question', 'fg:#2196F3 bold'),
|
139
|
+
('answer', 'fg:#FFB300 bold'),
|
140
|
+
('pointer', 'fg:#FFB300 bold'),
|
141
|
+
('highlighted', 'fg:#FFB300 bold'),
|
142
|
+
('selected', 'fg:#FFB300 bold'),
|
143
|
+
]
|
144
|
+
),
|
145
|
+
).ask()
|
146
|
+
save_problem(problem, Path(contest.name) / problem.label, session)
|
147
|
+
except ValueError as e:
|
148
|
+
print(f'[red][Error][/red] {e}')
|
239
149
|
|
240
150
|
elif choice == END:
|
241
151
|
print('[bold red]終了します[/]')
|
@@ -243,80 +153,70 @@ def interactive_download() -> None:
|
|
243
153
|
print('[bold red]無効な選択です[/]')
|
244
154
|
|
245
155
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
def
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
156
|
+
def plan_download(
|
157
|
+
args: List[str], session: requests.Session
|
158
|
+
) -> List[Tuple[Problem, Path]]:
|
159
|
+
def classify(arg):
|
160
|
+
try:
|
161
|
+
return Contest(arg, session)
|
162
|
+
except ValueError:
|
163
|
+
label = arg
|
164
|
+
return label
|
165
|
+
|
166
|
+
parsed: List[Union['Contest', str]] = list(map(classify, args))
|
167
|
+
|
168
|
+
groups: List[List[Union['Contest', str]]] = [
|
169
|
+
list(group)
|
170
|
+
for _, group in groupby(parsed, key=lambda x: isinstance(x, Contest))
|
171
|
+
]
|
172
|
+
|
173
|
+
if len(groups) == 1:
|
174
|
+
if all(isinstance(x, Contest) for x in groups[0]):
|
175
|
+
contests = cast(List[Contest], groups[0])
|
176
|
+
return [
|
177
|
+
(problem, Path(contest.name) / problem.label)
|
178
|
+
for contest in contests
|
179
|
+
for problem in contest.problems
|
180
|
+
]
|
181
|
+
else:
|
182
|
+
raise ValueError('コンテスト名を指定してください')
|
183
|
+
elif len(groups) == 2:
|
184
|
+
result = []
|
185
|
+
for i, j in product(groups[0], groups[1]):
|
186
|
+
if isinstance(i, Contest) and isinstance(j, str):
|
187
|
+
# Contest × Label
|
188
|
+
for problem in i.problems:
|
189
|
+
if problem.label == j:
|
190
|
+
result.append((problem, Path(i.name) / j))
|
191
|
+
elif isinstance(i, str) and isinstance(j, Contest):
|
192
|
+
# Label × Contest
|
193
|
+
for problem in j.problems:
|
194
|
+
if problem.label == i:
|
195
|
+
result.append((problem, Path(i) / j.name))
|
196
|
+
return result
|
197
|
+
else:
|
198
|
+
raise ValueError('ダウンロードの引数が正しくありません')
|
265
199
|
|
266
|
-
download 120..130 B
|
267
|
-
ABCの120~130番のB問題をダウンロード
|
268
200
|
|
269
|
-
|
270
|
-
|
201
|
+
@click.command(short_help='AtCoder の問題をダウンロード')
|
202
|
+
@click.argument('args', nargs=-1)
|
203
|
+
def download(args: List[str]) -> None:
|
271
204
|
"""
|
272
|
-
|
273
|
-
|
274
|
-
|
205
|
+
例:
|
206
|
+
download abc{001..012} {A..C}
|
207
|
+
download {A..E} abc{001..012}
|
208
|
+
"""
|
209
|
+
session = load_session()
|
275
210
|
|
276
|
-
if
|
277
|
-
|
278
|
-
|
279
|
-
except ValueError:
|
280
|
-
first = str(first)
|
281
|
-
problems = Contest(name=first).problems(session=load_session())
|
282
|
-
if not problems:
|
283
|
-
print(f'[red][Error][red/] コンテスト名が間違っています: {first}')
|
284
|
-
return
|
285
|
-
generate_problem_directory('.', problems, GenerateMode.gene_path_on_num)
|
286
|
-
return
|
211
|
+
if not args:
|
212
|
+
interactive_download(session)
|
213
|
+
return
|
287
214
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
"""
|
294
|
-
)
|
295
|
-
else:
|
296
|
-
second_args = convert_arg(str(second))
|
215
|
+
try:
|
216
|
+
plan = plan_download(args, session)
|
217
|
+
except ValueError as e:
|
218
|
+
print(f'[red][Error][/red] {e}')
|
219
|
+
return
|
297
220
|
|
298
|
-
|
299
|
-
|
300
|
-
second_args_diff = cast(List[Diff], second_args)
|
301
|
-
problems = [
|
302
|
-
Problem(Contest('abc', number), difficulty=diff)
|
303
|
-
for number in first_args_int
|
304
|
-
for diff in second_args_diff
|
305
|
-
]
|
306
|
-
generate_problem_directory(base_path, problems, GenerateMode.gene_path_on_num)
|
307
|
-
elif are_all_diffs(first_args) and are_all_integers(second_args):
|
308
|
-
first_args_diff = cast(List[Diff], first_args)
|
309
|
-
second_args_int = cast(List[int], second_args)
|
310
|
-
problems = [
|
311
|
-
Problem(Contest('abc', number), difficulty=diff)
|
312
|
-
for diff in first_args_diff
|
313
|
-
for number in second_args_int
|
314
|
-
]
|
315
|
-
generate_problem_directory(base_path, problems, GenerateMode.gene_path_on_diff)
|
316
|
-
else:
|
317
|
-
raise ValueError(
|
318
|
-
"""次のような形式で問題を指定してください
|
319
|
-
例 atcdr -d A 120..130 : A問題の120から130をダウンロードします
|
320
|
-
例 atcdr -d 120 : ABCのコンテストの問題をダウンロードします
|
321
|
-
"""
|
322
|
-
)
|
221
|
+
for prob, path in plan:
|
222
|
+
save_problem(prob, path, session)
|
atcdr/util/problem.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
import time
|
2
|
+
from dataclasses import dataclass
|
2
3
|
|
3
4
|
import requests
|
4
5
|
|
@@ -6,89 +7,41 @@ from atcdr.util.parse import get_problem_urls_from_tasks
|
|
6
7
|
|
7
8
|
|
8
9
|
class Contest:
|
9
|
-
def __init__(self, name: str,
|
10
|
+
def __init__(self, name: str, session: requests.Session):
|
10
11
|
if not name:
|
11
12
|
raise ValueError('nameは必須です')
|
12
|
-
self.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@property
|
28
|
-
def url(self) -> str:
|
29
|
-
return f'https://atcoder.jp/contests/{self.contest}/tasks'
|
30
|
-
|
31
|
-
def __str__(self) -> str:
|
32
|
-
return f'{self.contest}'
|
33
|
-
|
34
|
-
def __repr__(self) -> str:
|
35
|
-
return f'Contest(name={self._name}, number={self._number})'
|
36
|
-
|
37
|
-
def problems(self, session: Optional[requests.Session] = None) -> List['Problem']:
|
38
|
-
session = session or requests.Session()
|
39
|
-
response = session.get(self.url)
|
40
|
-
|
41
|
-
if response.status_code != 200:
|
42
|
-
return []
|
43
|
-
|
44
|
-
return [
|
45
|
-
Problem(self, label=label, url=url)
|
13
|
+
self.name = name
|
14
|
+
|
15
|
+
self.url = f'https://atcoder.jp/contests/{name}/tasks'
|
16
|
+
retry_attempts = 2
|
17
|
+
retry_wait = 0.30
|
18
|
+
for _ in range(retry_attempts):
|
19
|
+
response = session.get(self.url)
|
20
|
+
if response.ok:
|
21
|
+
break
|
22
|
+
else:
|
23
|
+
time.sleep(retry_wait)
|
24
|
+
|
25
|
+
self.problems = [
|
26
|
+
Problem(url=url, contest=self, label=label)
|
46
27
|
for label, url in get_problem_urls_from_tasks(response.text)
|
47
28
|
]
|
48
29
|
|
49
|
-
|
50
|
-
|
51
|
-
def __new__(cls, diff: str) -> 'Diff':
|
52
|
-
if isinstance(diff, str) and len(diff) == 1 and diff.isalpha():
|
53
|
-
return super().__new__(cls, diff.upper())
|
54
|
-
raise ValueError('diffは英大文字または小文字の1文字である必要があります')
|
30
|
+
def __str__(self) -> str:
|
31
|
+
return f'{self.name}'
|
55
32
|
|
56
33
|
def __repr__(self) -> str:
|
57
|
-
return f
|
34
|
+
return f'Contest(name={self.name})'
|
58
35
|
|
59
36
|
|
37
|
+
@dataclass
|
60
38
|
class Problem:
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
difficulty: Optional[Diff] = None,
|
65
|
-
label: Optional[str] = None,
|
66
|
-
url: Optional[str] = None,
|
67
|
-
):
|
68
|
-
self._contest = contest
|
69
|
-
if difficulty:
|
70
|
-
self._label = difficulty.upper()
|
71
|
-
self._url = contest.url + f'/{contest}_{difficulty.lower()}'
|
72
|
-
elif label and url:
|
73
|
-
self._label = label
|
74
|
-
self._url = url
|
75
|
-
else:
|
76
|
-
raise ValueError('labelとurlは両方必須かdifficultyが必要です')
|
77
|
-
|
78
|
-
@property
|
79
|
-
def contest(self) -> Contest:
|
80
|
-
return self._contest
|
39
|
+
url: str
|
40
|
+
contest: Contest
|
41
|
+
label: str
|
81
42
|
|
82
|
-
|
83
|
-
|
84
|
-
return self._label
|
85
|
-
|
86
|
-
@property
|
87
|
-
def url(self) -> str:
|
88
|
-
return self._url
|
43
|
+
def __str__(self) -> str:
|
44
|
+
return f'{self.label} - {self.contest.name}'
|
89
45
|
|
90
46
|
def __repr__(self) -> str:
|
91
|
-
return f'Problem(
|
92
|
-
|
93
|
-
def __str__(self) -> str:
|
94
|
-
return f'{self.contest} {self.label}'
|
47
|
+
return f'Problem(url={self.url}, contest={self.contest}, label={self.label})'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: AtCoderStudyBooster
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.0
|
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>
|
@@ -8,7 +8,7 @@ License: MIT
|
|
8
8
|
Requires-Python: >=3.8
|
9
9
|
Requires-Dist: beautifulsoup4
|
10
10
|
Requires-Dist: click-aliases>=1.0.5
|
11
|
-
Requires-Dist: markdownify
|
11
|
+
Requires-Dist: markdownify==0.13.1
|
12
12
|
Requires-Dist: pywebview>=5.4
|
13
13
|
Requires-Dist: questionary>=2.0.1
|
14
14
|
Requires-Dist: requests
|
@@ -25,86 +25,126 @@ Description-Content-Type: text/markdown
|
|
25
25
|
|
26
26
|
🚧 このプロジェクトはまだ実験段階です。日々のAtCoder学習に役立つ機能を順次追加しています。
|
27
27
|
|
28
|
-
AtCoderStudyBoosterはAtCoderの学習を加速させるためのCLIツールです。問題をローカルにダウンロードし、テスト、提出、解答の作成をサポートするツールです。Pythonが入っていることが必須です。Python
|
28
|
+
AtCoderStudyBoosterはAtCoderの学習を加速させるためのCLIツールです。問題をローカルにダウンロードし、テスト、提出、解答の作成をサポートするツールです。Pythonが入っていることが必須です。Pythonが入っている環境なら、
|
29
29
|
|
30
|
-
|
30
|
+
```sh
|
31
|
+
pip install AtCoderStudyBooster
|
32
|
+
```
|
31
33
|
|
32
|
-
|
33
|
-
[online-judge-tools](https://github.com/online-judge-tools)
|
34
|
-
[atcoder-cli](https://github.com/Tatamo/atcoder-cli)
|
34
|
+
でインストールできます。(Python3.8以上が必要です)
|
35
35
|
|
36
|
+
キャプチャ認証が導入されたあとでも、CLIから半自動でログイン&提出できます。ただしキャプチャをGUIから人力で解く必要があります。キャプチャー認証をバイパスするものではありません。
|
37
|
+
|
38
|
+
このツールは以下のプロジェクトに強く影響を受けています。
|
39
|
+
- [online-judge-tools](https://github.com/online-judge-tools)
|
40
|
+
- [atcoder-cli](https://github.com/Tatamo/atcoder-cli)
|
36
41
|
|
37
42
|
## 利用ケース
|
38
43
|
|
39
44
|
まずは`download`コマンドを利用して問題をローカルにダウンロードしてみましょう。
|
40
45
|
|
41
|
-
###
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
```
|
52
|
-
|
53
|
-
├── 223
|
54
|
-
│ ├── StringShifting.html
|
55
|
-
│ └── StringShifting.md
|
56
|
-
├── 224
|
57
|
-
│ ├── Mongeness.html
|
58
|
-
│ └── Mongeness.md
|
59
|
-
├── 225
|
60
|
-
│ ├── StarorNot.html
|
61
|
-
│ └── StarorNot.md
|
62
|
-
└── 226
|
63
|
-
├── CountingArrays.html
|
64
|
-
└── CountingArrays.md
|
65
|
-
```
|
66
|
-
|
67
|
-
### 特定のコンテストの問題に取り組みたい場合
|
68
|
-
|
69
|
-
```sh
|
70
|
-
❯ atcdr download 223..225 A..C
|
71
|
-
```
|
72
|
-
のように実行すると以下のようなフォルダーを生成します.
|
73
|
-
|
74
|
-
```css
|
75
|
-
.
|
76
|
-
├── 223
|
77
|
-
│ ├── A
|
78
|
-
│ │ ├── ExactPrice.html
|
79
|
-
│ │ └── ExactPrice.md
|
80
|
-
│ ├── B
|
81
|
-
│ │ ├── StringShifting.html
|
82
|
-
│ │ └── StringShifting.md
|
83
|
-
│ └── C
|
84
|
-
│ ├── Doukasen.html
|
85
|
-
│ └── Doukasen.md
|
86
|
-
├── 224
|
87
|
-
│ ├── A
|
88
|
-
│ │ ├── Tires.html
|
89
|
-
│ │ └── Tires.md
|
90
|
-
│ ├── B
|
91
|
-
│ │ ├── Mongeness.html
|
92
|
-
│ │ └── Mongeness.md
|
93
|
-
│ └── C
|
94
|
-
│ ├── Triangle.html
|
95
|
-
│ └── Triangle.md
|
96
|
-
└── 225
|
97
|
-
├── A
|
98
|
-
│ ├── DistinctStrings.html
|
99
|
-
│ └── DistinctStrings.md
|
100
|
-
├── B
|
101
|
-
│ ├── StarorNot.html
|
102
|
-
│ └── StarorNot.md
|
103
|
-
└── C
|
104
|
-
├── CalendarValidator.html
|
105
|
-
└── CalendarValidator.md
|
46
|
+
### 1. 特定のコンテストをダウンロードする
|
47
|
+
|
48
|
+
1つのコンテストの問題をダウンロードしたい場合の例です。
|
49
|
+
|
50
|
+
#### ABC350の全問題をダウンロード
|
51
|
+
```sh
|
52
|
+
❯ atcdr download abc350
|
53
|
+
```
|
54
|
+
|
55
|
+
#### ABC350のA〜D問題をダウンロード
|
56
|
+
```sh
|
57
|
+
❯ atcdr download abc350 {A..D}
|
106
58
|
```
|
107
59
|
|
60
|
+
#### 競プロ典型90問のダウンロード
|
61
|
+
```sh
|
62
|
+
❯ atcdr download typical90
|
63
|
+
```
|
64
|
+
|
65
|
+
### 2. 複数のコンテストを一括でダウンロードする
|
66
|
+
|
67
|
+
複数のコンテストを一度にダウンロードしたい場合の例です。bashのブレース展開を活用します。
|
68
|
+
|
69
|
+
#### ABC001〜ABC010までの全問題
|
70
|
+
```sh
|
71
|
+
❯ atcdr download abc{001..010}
|
72
|
+
```
|
73
|
+
|
74
|
+
#### ABC320〜ABC325までのA〜C問題
|
75
|
+
```sh
|
76
|
+
❯ atcdr download abc{320..325} {A..C}
|
77
|
+
```
|
78
|
+
|
79
|
+
次のようなフォルダー構造が生成されます:
|
80
|
+
|
81
|
+
```
|
82
|
+
abc320/
|
83
|
+
├── A/
|
84
|
+
│ ├── Problem.html
|
85
|
+
│ └── Problem.md
|
86
|
+
├── B/
|
87
|
+
│ ├── Problem.html
|
88
|
+
│ └── Problem.md
|
89
|
+
└── C/
|
90
|
+
├── Problem.html
|
91
|
+
└── Problem.md
|
92
|
+
abc321/
|
93
|
+
├── A/
|
94
|
+
...(以下同様)
|
95
|
+
```
|
96
|
+
|
97
|
+
### 3. 特定の問題(A問題、B問題など)を集中的に演習したい場合
|
98
|
+
|
99
|
+
特定の難易度の問題だけを集めて練習したい場合は、**問題ラベルを先に指定**すると便利です。
|
100
|
+
|
101
|
+
#### B問題だけを集中的に練習
|
102
|
+
```sh
|
103
|
+
❯ atcdr download B abc{250..260}
|
104
|
+
```
|
105
|
+
|
106
|
+
問題ラベルを先に指定すると、問題ラベルごとにフォルダが作成され、その中にコンテスト名のフォルダが配置されます。
|
107
|
+
|
108
|
+
```
|
109
|
+
B/
|
110
|
+
├── abc250/
|
111
|
+
│ ├── Problem.html
|
112
|
+
│ └── Problem.md
|
113
|
+
├── abc251/
|
114
|
+
│ ├── Problem.html
|
115
|
+
│ └── Problem.md
|
116
|
+
├── abc252/
|
117
|
+
│ ├── Problem.html
|
118
|
+
│ └── Problem.md
|
119
|
+
└── ...(以下同様)
|
120
|
+
```
|
121
|
+
|
122
|
+
A問題とB問題を集める場合(`{A,B} abc{300..302}`):
|
123
|
+
|
124
|
+
```
|
125
|
+
A/
|
126
|
+
├── abc300/
|
127
|
+
│ ├── Problem.html
|
128
|
+
│ └── Problem.md
|
129
|
+
├── abc301/
|
130
|
+
│ ├── Problem.html
|
131
|
+
│ └── Problem.md
|
132
|
+
└── abc302/
|
133
|
+
├── Problem.html
|
134
|
+
└── Problem.md
|
135
|
+
B/
|
136
|
+
├── abc300/
|
137
|
+
│ ├── Problem.html
|
138
|
+
│ └── Problem.md
|
139
|
+
├── abc301/
|
140
|
+
│ ├── Problem.html
|
141
|
+
│ └── Problem.md
|
142
|
+
└── abc302/
|
143
|
+
├── Problem.html
|
144
|
+
└── Problem.md
|
145
|
+
```
|
146
|
+
このディレクトリ構造により、同じ難易度の問題を一箇所に集めて効率的に演習できます。
|
147
|
+
|
108
148
|
### 問題を解く
|
109
149
|
|
110
150
|
MarkdownファイルあるいはHTMLファイルをVS CodeのHTML Preview, Markdown Previewで開くと問題を確認できます。VS Codeで開くと左側にテキストエディターを表示して、右側で問題をみながら問題に取り組めます。
|
@@ -116,13 +156,13 @@ MarkdownファイルあるいはHTMLファイルをVS CodeのHTML Preview, Markd
|
|
116
156
|
問題をダウンロードしたフォルダーに移動します。
|
117
157
|
|
118
158
|
```sh
|
119
|
-
❯ cd
|
159
|
+
❯ cd abc224/B
|
120
160
|
```
|
121
161
|
|
122
|
-
|
162
|
+
移動したフォルダーで解答ファイルを作成後、testコマンドを実行すると、サンプルケースをテストします。
|
123
163
|
|
124
164
|
```sh
|
125
|
-
~/.../
|
165
|
+
~/.../abc224/B
|
126
166
|
❯ atcdr t
|
127
167
|
```
|
128
168
|
|
@@ -132,82 +172,90 @@ WAの場合は以下のような表示になります。
|
|
132
172
|
|
133
173
|

|
134
174
|
|
135
|
-
|
136
175
|
### 提出する
|
137
176
|
|
138
177
|
```sh
|
139
|
-
~/.../
|
178
|
+
~/.../abc224/B
|
140
179
|
❯ atcdr s
|
141
180
|
```
|
142
|
-
|
181
|
+
|
182
|
+
を実行すると、提出することができます。提出にはAtCoderのサイトへのログインが必要です。
|
143
183
|
|
144
184
|
### 解答をGPTで生成する
|
145
185
|
|
146
186
|
```sh
|
147
|
-
~/.../
|
187
|
+
~/.../abc224/B
|
148
188
|
❯ atcdr g
|
149
189
|
```
|
190
|
+
|
150
191
|
で解答をGPTで生成します。Chat GPTのAPIキーが必要です。さらに、生成されたファイルはサンプルケースが自動でテストされ、**テストをパスしなかった場合、テスト結果がGPTにフィードバックされ解答が再生成**されます。
|
151
192
|
|
152
193
|
GPTとプログラムとのやり取りのログはJSONファイルで保存されます。
|
153
194
|
|
154
195
|
## 解答生成機能generateコマンドに関する注意点
|
155
196
|
|
156
|
-
[AtCoder生成AI対策ルール](https://info.atcoder.jp/entry/llm-rules-ja)によるとAtCoder Beginner Contest(以下、ABCとする)および AtCoder Regular Contest (Div. 2) においてに問題文を生成AI
|
197
|
+
[AtCoder生成AI対策ルール](https://info.atcoder.jp/entry/llm-rules-ja)によるとAtCoder Beginner Contest(以下、ABCとする)および AtCoder Regular Contest (Div. 2) においてに問題文を生成AIに直接与えることは禁止されています。ただし、このルールは過去問を練習している際には適用されません。該当のコンテスト中にこの機能を使用しないでください。
|
157
198
|
|
158
199
|
## その他の機能
|
159
200
|
|
160
201
|
### markdownコマンド
|
161
202
|
|
162
203
|
完全なCLI環境方向けのコマンドです。
|
204
|
+
|
163
205
|
```sh
|
164
|
-
~/.../
|
206
|
+
~/.../abc224/B
|
165
207
|
❯ atcdr md
|
166
208
|
```
|
167
|
-
|
209
|
+
|
210
|
+
を実行すると、問題をプリントします。
|
168
211
|
|
169
212
|

|
170
213
|
|
171
214
|
### 複数のファイルを一度にテスト
|
172
215
|
|
173
216
|
```sh
|
174
|
-
~/.../
|
217
|
+
~/.../abc224/B
|
175
218
|
❯ atcdr t *.py
|
176
219
|
```
|
220
|
+
|
177
221
|
でフォルダー内にあるすべてのPythonファイルを一度にテストします。
|
222
|
+
|
178
223
|
```sh
|
179
|
-
~/.../
|
224
|
+
~/.../abc224/B
|
180
225
|
❯ atcdr t mon.py mon.c mon.cpp
|
181
226
|
```
|
182
227
|
|
183
228
|
フォルダー内に複数ファイルある場合は、インタラクティブに選択できます。
|
229
|
+
|
184
230
|
```sh
|
185
|
-
~/.../
|
231
|
+
~/.../abc224/B
|
186
232
|
❯ atcdr t
|
187
233
|
```
|
188
234
|
|
189
235
|
```sh
|
190
|
-
~/.../
|
236
|
+
~/.../abc224/B
|
191
237
|
❯ atcdr t
|
192
|
-
|
238
|
+
複数のファイルが見つかりました.ファイルを選択してください:
|
193
239
|
十字キーで移動, [enter]で実行
|
194
|
-
|
195
|
-
|
196
|
-
|
240
|
+
❯❯❯ mon.py
|
241
|
+
mon.c
|
242
|
+
mon.cpp
|
197
243
|
```
|
198
244
|
|
199
245
|
### プログラミング言語を指定してコードを生成
|
200
246
|
|
201
247
|
`--lang`オプションを使うと、生成したいプログラミング言語を指定できます。
|
248
|
+
|
202
249
|
```sh
|
203
|
-
~/.../
|
250
|
+
~/.../abc224/B
|
204
251
|
❯ atcdr generate --lang rust
|
205
252
|
```
|
253
|
+
|
206
254
|
### テストをせずにコードのみ生成
|
207
255
|
|
208
256
|
デフォルトで`atcdr generate`コマンドは生成されたコードをテストしますが、テストせずにコードのみ生成できます。
|
209
257
|
|
210
258
|
```sh
|
211
|
-
~/.../
|
259
|
+
~/.../abc224/B
|
212
260
|
❯ atcdr generate --lang rust --without_test
|
213
261
|
```
|
@@ -1,6 +1,6 @@
|
|
1
1
|
atcdr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
atcdr/cli.py,sha256=xiCnGf14VHtLcRlVHXNpLKW3FIxOHS2duTRDTsG8a5w,2595
|
3
|
-
atcdr/download.py,sha256=
|
3
|
+
atcdr/download.py,sha256=R7hdhYupw3hppqjKpt7kchT-V2AJqHypitfb2qIwvtU,7926
|
4
4
|
atcdr/generate.py,sha256=i-rznjOQpBcAmlI0CGGC8CEY3Nx35SoxQgGaPSh6Oz8,8183
|
5
5
|
atcdr/login.py,sha256=gro9gj-nF4pGz4KepkaNSMj8PRGdSJtaVcBe7YKUPzU,4697
|
6
6
|
atcdr/logout.py,sha256=WYFifFDZtdcFZ0z3BhqsmdK_JbufjmBdF4q5dDkLbHQ,753
|
@@ -13,9 +13,9 @@ atcdr/util/fileops.py,sha256=jxJ_d-R3XG-CLQemwwxgeNQcvNAE_7Q-3nOQ1npG3ig,3293
|
|
13
13
|
atcdr/util/filetype.py,sha256=pceB08trkwNkdzKJx4fFfOWpz9jYMBRDwXLW4un0sRM,2254
|
14
14
|
atcdr/util/gpt.py,sha256=vH10Waa7KXlz6-5pEUBuIbxTbDH3Hys7k9Tt7prvFX4,3772
|
15
15
|
atcdr/util/parse.py,sha256=SJ4khlH5iWaSwyORmQLi84npMwh5u2omCeg0q7ScEAE,6974
|
16
|
-
atcdr/util/problem.py,sha256=
|
16
|
+
atcdr/util/problem.py,sha256=OERGf1uw4DIe4ZA6OPwt2ASoMxho2ped5HZ4fIb2XSE,1189
|
17
17
|
atcdr/util/session.py,sha256=LwlSN86sfnkE9a-opL4qvKYHsCgiMy7eFCdcXdo79eg,4889
|
18
|
-
atcoderstudybooster-0.
|
19
|
-
atcoderstudybooster-0.
|
20
|
-
atcoderstudybooster-0.
|
21
|
-
atcoderstudybooster-0.
|
18
|
+
atcoderstudybooster-0.4.0.dist-info/METADATA,sha256=J5wZhLmMh333Kgcaz_QBPQQIc6UB_BMZlbu_zKwsqEU,7641
|
19
|
+
atcoderstudybooster-0.4.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
20
|
+
atcoderstudybooster-0.4.0.dist-info/entry_points.txt,sha256=-stL-IwnheQGlYAdm82RuZu8CGgSakU0aVIVlA7DmFA,40
|
21
|
+
atcoderstudybooster-0.4.0.dist-info/RECORD,,
|
File without changes
|
{atcoderstudybooster-0.3.2.dist-info → atcoderstudybooster-0.4.0.dist-info}/entry_points.txt
RENAMED
File without changes
|