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 CHANGED
@@ -1,22 +1,24 @@
1
- import os
2
1
  import re
3
2
  import time
4
- from typing import Callable, List, Union, cast
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, Diff, Problem
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 = load_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 generate_problem_directory(
88
- base_path: str, problems: List[Problem], gene_path: Callable[[str, Problem], str]
89
- ) -> None:
90
- downloader = Downloader()
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
- def parse_range(match: re.Match) -> List[int]:
118
- start, end = map(int, match.groups())
119
- start, end = min(start, end), max(start, end)
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
- def are_all_integers(args: Union[List[int], List[Diff]]) -> bool:
144
- return all(isinstance(arg, int) for arg in args)
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
- def are_all_diffs(args: Union[List[int], List[Diff]]) -> bool:
148
- return all(isinstance(arg, Diff) for arg in args)
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
- PRACTICE = '2. 特定の難易度の問題を集中的に練習したい'
154
- ONE_FILE = '3. 1問だけダウンロードする'
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, PRACTICE, ONE_FILE, END],
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
- difficulty = Diff(difficulty)
192
- except KeyError:
193
- raise ValueError('入力された難易度が認識できません')
194
- number_str = Prompt.ask(
195
- 'コンテスト番号または範囲を入力してください (例: 120..130)'
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
- 'コンテスト名を入力してください (例: abc012, abs, typical90)',
214
- )
215
-
216
- problems = Contest(name=name).problems(session=load_session())
217
-
218
- problem = q.select(
219
- message='どの問題をダウンロードしますか?',
220
- qmark='',
221
- pointer='❯❯❯',
222
- choices=[
223
- q.Choice(title=f'{problem.label:10} | {problem.url}', value=problem)
224
- for problem in problems
225
- ],
226
- instruction='\n 十字キーで移動,[enter]で実行',
227
- style=q.Style(
228
- [
229
- ('question', 'fg:#2196F3 bold'),
230
- ('answer', 'fg:#FFB300 bold'),
231
- ('pointer', 'fg:#FFB300 bold'),
232
- ('highlighted', 'fg:#FFB300 bold'),
233
- ('selected', 'fg:#FFB300 bold'),
234
- ]
235
- ),
236
- ).ask()
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
- @click.command(short_help='AtCoderの問題をダウンロード')
247
- @click.argument('first', nargs=1, type=str, required=False)
248
- @click.argument('second', nargs=1, type=str, required=False)
249
- def download(
250
- first: Union[str, None] = None,
251
- second: Union[str, None] = None,
252
- base_path: str = '.',
253
- ) -> None:
254
- """
255
- AtCoderの問題をダウンロードします
256
-
257
- download
258
- 対話形式でダウンロードを開始します。
259
-
260
- download abc012
261
- コンテスト abc012 の全問題をダウンロード
262
-
263
- download A 120
264
- 難易度Aの120番問題をダウンロード
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
- download 120
270
- ABCの120番問題をダウンロード
201
+ @click.command(short_help='AtCoder の問題をダウンロード')
202
+ @click.argument('args', nargs=-1)
203
+ def download(args: List[str]) -> None:
271
204
  """
272
- if first is None:
273
- interactive_download()
274
- return
205
+ 例:
206
+ download abc{001..012} {A..C}
207
+ download {A..E} abc{001..012}
208
+ """
209
+ session = load_session()
275
210
 
276
- if second is None:
277
- try:
278
- first_args = convert_arg(str(first))
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
- if are_all_diffs(first_args):
289
- raise ValueError(
290
- """難易度だけでなく, 問題番号も指定してコマンドを実行してください.
291
- atcdr -d A 120 : A問題の120をダウンロードます
292
- 例 atcdr -d A 120..130 : A問題の120から130をダウンロードます
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
- if are_all_integers(first_args) and are_all_diffs(second_args):
299
- first_args_int = cast(List[int], first_args)
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
- from typing import List, Optional
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, number: Optional[int] = None):
10
+ def __init__(self, name: str, session: requests.Session):
10
11
  if not name:
11
12
  raise ValueError('nameは必須です')
12
- self._name = name
13
- self._number = number
14
- if number and number > 0:
15
- self._contest = f'{name}{number:03}'
16
- else:
17
- self._contest = name
18
-
19
- @property
20
- def contest(self) -> str:
21
- return self._contest
22
-
23
- @property
24
- def number(self) -> Optional[int]:
25
- return self._number
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
- class Diff(str):
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"Diff('{self}')"
34
+ return f'Contest(name={self.name})'
58
35
 
59
36
 
37
+ @dataclass
60
38
  class Problem:
61
- def __init__(
62
- self,
63
- contest: Contest,
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
- @property
83
- def label(self) -> str:
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(contest={self.contest}, label={self.label}, url={self.url})'
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.2
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>=0.13.1
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が入っている環境なら、`pip install AtCoderStudyBooster`でインストールできます。(Python3.8以上が必要です)
28
+ AtCoderStudyBoosterはAtCoderの学習を加速させるためのCLIツールです。問題をローカルにダウンロードし、テスト、提出、解答の作成をサポートするツールです。Pythonが入っていることが必須です。Pythonが入っている環境なら、
29
29
 
30
- キャプチャ認証が導入されたあとでも、CLIからほぼ自動でログイン&提出できます。ただしキャプチャを解くために、GUI環境が必要になる場合があります。
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
- ### B問題の練習したい場合
42
-
43
- ABCコンテストの223から226のB問題だけを集中的に練習したい場合、次のコマンドを実行します。
44
-
45
- ```sh
46
- ❯ atcdr download B 223..226
47
- ```
48
-
49
- コマンドを実行すると,次のようなフォルダーを作成して、各々のフォルダーに問題をダウンロードします。
50
-
51
- ```css
52
- B
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 224/B
159
+ ❯ cd abc224/B
120
160
  ```
121
161
 
122
- 移動したフォルダーで解答ファイルを作成後をtestコマンドを実行すると, サンプルケースをテストします。
162
+ 移動したフォルダーで解答ファイルを作成後、testコマンドを実行すると、サンプルケースをテストします。
123
163
 
124
164
  ```sh
125
- ~/.../224/B
165
+ ~/.../abc224/B
126
166
  ❯ atcdr t
127
167
  ```
128
168
 
@@ -132,82 +172,90 @@ WAの場合は以下のような表示になります。
132
172
 
133
173
  ![demo画像](./.images/demo3.png)
134
174
 
135
-
136
175
  ### 提出する
137
176
 
138
177
  ```sh
139
- ~/.../224/B
178
+ ~/.../abc224/B
140
179
  ❯ atcdr s
141
180
  ```
142
- を実行すると, 提出することができます。提出にはAtCoderのサイトへのログインが必要です。
181
+
182
+ を実行すると、提出することができます。提出にはAtCoderのサイトへのログインが必要です。
143
183
 
144
184
  ### 解答をGPTで生成する
145
185
 
146
186
  ```sh
147
- ~/.../224/B
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
- ~/.../224/B
206
+ ~/.../abc224/B
165
207
  ❯ atcdr md
166
208
  ```
167
- を実行すると, 問題をプリントします。
209
+
210
+ を実行すると、問題をプリントします。
168
211
 
169
212
  ![demo画像](./.images/demo4.png)
170
213
 
171
214
  ### 複数のファイルを一度にテスト
172
215
 
173
216
  ```sh
174
- ~/.../224/B
217
+ ~/.../abc224/B
175
218
  ❯ atcdr t *.py
176
219
  ```
220
+
177
221
  でフォルダー内にあるすべてのPythonファイルを一度にテストします。
222
+
178
223
  ```sh
179
- ~/.../224/B
224
+ ~/.../abc224/B
180
225
  ❯ atcdr t mon.py mon.c mon.cpp
181
226
  ```
182
227
 
183
228
  フォルダー内に複数ファイルある場合は、インタラクティブに選択できます。
229
+
184
230
  ```sh
185
- ~/.../224/B
231
+ ~/.../abc224/B
186
232
  ❯ atcdr t
187
233
  ```
188
234
 
189
235
  ```sh
190
- ~/.../224/B
236
+ ~/.../abc224/B
191
237
  ❯ atcdr t
192
- 複数のファイルが見つかりました.ファイルを選択してください:
238
+ 複数のファイルが見つかりました.ファイルを選択してください:
193
239
  十字キーで移動, [enter]で実行
194
- ❯❯❯ mon.py
195
- mon.c
196
- mon.cpp
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
- ~/.../224/B
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
- ~/.../224/B
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=gJ5ZPQJd06fxWOUzC_GFBY-d7KIyaURoPwO86_QdU4A,11711
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=wEoMC5KVQu5l-i4V-ZCR30Z30Zov0VCNtLVIbL-0iI0,2594
16
+ atcdr/util/problem.py,sha256=OERGf1uw4DIe4ZA6OPwt2ASoMxho2ped5HZ4fIb2XSE,1189
17
17
  atcdr/util/session.py,sha256=LwlSN86sfnkE9a-opL4qvKYHsCgiMy7eFCdcXdo79eg,4889
18
- atcoderstudybooster-0.3.2.dist-info/METADATA,sha256=2I0SCiBGyWTco6e434Cs6AKSsGlLYinXxQbgVGu2N74,6785
19
- atcoderstudybooster-0.3.2.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
20
- atcoderstudybooster-0.3.2.dist-info/entry_points.txt,sha256=-stL-IwnheQGlYAdm82RuZu8CGgSakU0aVIVlA7DmFA,40
21
- atcoderstudybooster-0.3.2.dist-info/RECORD,,
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,,