AtCoderStudyBooster 0.3__py3-none-any.whl → 0.3.2__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/cli.py ADDED
@@ -0,0 +1,86 @@
1
+ from importlib.metadata import metadata
2
+
3
+ import rich_click as click
4
+ from click_aliases import ClickAliasedGroup
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+ from rich.traceback import install
9
+ from rich_click import RichGroup
10
+
11
+ from atcdr.download import download
12
+ from atcdr.generate import generate
13
+ from atcdr.login import login
14
+ from atcdr.logout import logout
15
+ from atcdr.markdown import markdown
16
+ from atcdr.open import open_files
17
+ from atcdr.submit import submit
18
+ from atcdr.test import test
19
+
20
+
21
+ # ─── RichClick + ClickAliases 両対応の Group クラス ───
22
+ class AliasedRichGroup(ClickAliasedGroup, RichGroup):
23
+ def format_commands(self, ctx, console, *args, **kwargs):
24
+ console = Console()
25
+ commands = self.list_commands(ctx)
26
+
27
+ table = Table(show_header=False, box=None, pad_edge=False)
28
+ table.add_column('command', style='bold cyan', no_wrap=True)
29
+ table.add_column('help', style='')
30
+
31
+ for name in commands:
32
+ cmd = self.get_command(ctx, name)
33
+ if not cmd or getattr(cmd, 'hidden', False):
34
+ continue
35
+
36
+ aliases = self._commands.get(name, [])
37
+ alias_part = f"[dim]({', '.join(aliases)})[/]" if aliases else ''
38
+
39
+ short = (
40
+ cmd.get_short_help_str()
41
+ if hasattr(cmd, 'get_short_help_str')
42
+ else cmd.short_help or ''
43
+ )
44
+ table.add_row(f'{name}{alias_part}', short)
45
+
46
+ panel = Panel(table, title='Commands', expand=False)
47
+ console.print(panel)
48
+
49
+
50
+ # ─── CLI 定義 ──────────────────────────────────────────
51
+ _meta = metadata('AtCoderStudyBooster')
52
+ _NAME = _meta['Name']
53
+ _VERSION = _meta['Version']
54
+
55
+ click.rich_click.MAX_WIDTH = 100
56
+ click.rich_click.SHOW_ARGUMENTS = True
57
+ click.rich_click.STYLE_HELPTEXT_FIRST_LINE = 'bold cyan'
58
+ click.rich_click.STYLE_HELPTEXT = 'dim'
59
+
60
+
61
+ @click.group(
62
+ cls=AliasedRichGroup,
63
+ context_settings={'help_option_names': ['-h', '--help']},
64
+ )
65
+ @click.version_option(
66
+ _VERSION,
67
+ '-v',
68
+ '--version',
69
+ prog_name=_NAME,
70
+ message='%(prog)s %(version)s',
71
+ )
72
+ def cli():
73
+ install()
74
+
75
+
76
+ cli.add_command(test, aliases=['t'])
77
+ cli.add_command(download, aliases=['d'])
78
+ cli.add_command(open_files, 'open', aliases=['o'])
79
+ cli.add_command(generate, aliases=['g'])
80
+ cli.add_command(markdown, aliases=['md'])
81
+ cli.add_command(submit, aliases=['s'])
82
+ cli.add_command(login)
83
+ cli.add_command(logout)
84
+
85
+ if __name__ == '__main__':
86
+ cli()
atcdr/download.py CHANGED
@@ -1,289 +1,322 @@
1
1
  import os
2
2
  import re
3
3
  import time
4
- from dataclasses import dataclass
5
- from enum import Enum
6
- from typing import Callable, List, Optional, Union, cast
4
+ from typing import Callable, List, Union, cast
7
5
 
8
6
  import questionary as q
9
- import requests
10
- from rich.console import Console
11
- from rich.prompt import IntPrompt, Prompt
7
+ import rich_click as click
8
+ from rich import print
9
+ from rich.prompt import Prompt
12
10
 
13
11
  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}')
12
+ from atcdr.util.parse import ProblemHTML
13
+ from atcdr.util.problem import Contest, Diff, Problem
14
+ from atcdr.util.session import load_session
15
+
16
+
17
+ class Downloader:
18
+ def __init__(self) -> None:
19
+ self.session = load_session()
20
+
21
+ def get(self, problem: Problem) -> ProblemHTML:
22
+ session = self.session
23
+ retry_attempts = 3
24
+ retry_wait = 1 # 1 second
25
+
26
+ for _ in range(retry_attempts):
27
+ response = session.get(problem.url)
28
+ if response.status_code == 200:
29
+ return ProblemHTML(response.text)
30
+ elif response.status_code == 429:
31
+ print(
32
+ f'[bold yellow][Error {response.status_code}][/bold yellow] 再試行します。{problem}'
33
+ )
34
+ time.sleep(retry_wait)
35
+ elif 300 <= response.status_code < 400:
36
+ print(
37
+ f'[bold yellow][Error {response.status_code}][/bold yellow] リダイレクトが発生しました。{problem}'
38
+ )
39
+ elif 400 <= response.status_code < 500:
40
+ print(
41
+ f'[bold red][Error {response.status_code}][/bold red] 問題が見つかりません。{problem}'
42
+ )
43
+ break
44
+ elif 500 <= response.status_code < 600:
45
+ print(
46
+ f'[bold red][Error {response.status_code}][/bold red] サーバーエラーが発生しました。{problem}'
47
+ )
48
+ break
49
+ else:
50
+ print(
51
+ f'[bold red][Error {response.status_code}][/bold red] {problem}に対応するHTMLファイルを取得できませんでした。'
52
+ )
53
+ break
54
+ return ProblemHTML('')
81
55
 
82
56
 
83
57
  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} を作成しました')
58
+ if not os.path.exists(path):
59
+ os.makedirs(path)
60
+ print(f'[bold green][+][/bold green] フォルダー: {path} を作成しました')
87
61
 
88
62
 
89
63
  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))
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
+ )
93
71
 
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)
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
+ def title_to_filename(title: str) -> str:
82
+ title = re.sub(r'[\\/*?:"<>| !@#$%^&()+=\[\]{};,\']', '', title)
83
+ title = re.sub(r'.*?-', '', title)
84
+ return title
97
85
 
98
86
 
99
87
  def generate_problem_directory(
100
- base_path: str, problems: List[Problem], gene_path: Callable[[str, int, Diff], str]
88
+ base_path: str, problems: List[Problem], gene_path: Callable[[str, Problem], str]
101
89
  ) -> None:
102
- for problem in problems:
103
- dir_path = gene_path(base_path, problem.number, problem.difficulty)
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
104
96
 
105
- html = get_problem_html(problem)
106
- if html is None:
107
- continue
97
+ dir_path = gene_path(base_path, problem)
98
+ mkdir(dir_path)
108
99
 
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}'
100
+ problem_content.repair_me()
113
101
 
114
- title = title_to_filename(title)
102
+ title = problem_content.title or problem.label
103
+ title = title_to_filename(title)
115
104
 
116
- mkdir(dir_path)
117
- repaired_html = repair_html(html)
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}')
118
109
 
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)
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}')
124
115
 
125
116
 
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('数字の範囲の形式が間違っています')
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))
133
121
 
134
122
 
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 の形式になっていません')
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)]
144
128
 
145
129
 
146
130
  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}は認識できません')
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}は認識できません')
159
141
 
160
142
 
161
143
  def are_all_integers(args: Union[List[int], List[Diff]]) -> bool:
162
- return all(isinstance(arg, int) for arg in args)
144
+ return all(isinstance(arg, int) for arg in args)
163
145
 
164
146
 
165
147
  def are_all_diffs(args: Union[List[int], List[Diff]]) -> bool:
166
- return all(isinstance(arg, Diff) for arg in args)
148
+ return all(isinstance(arg, Diff) for arg in args)
167
149
 
168
150
 
169
151
  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
-
152
+ CONTEST = '1. コンテストの問題を解きたい'
153
+ PRACTICE = '2. 特定の難易度の問題を集中的に練習したい'
154
+ ONE_FILE = '3. 1問だけダウンロードする'
155
+ END = '4. 終了する'
156
+
157
+ choice = q.select(
158
+ message='AtCoderの問題のHTMLファイルをダウンロードします',
159
+ qmark='',
160
+ pointer='❯❯❯',
161
+ choices=[CONTEST, PRACTICE, ONE_FILE, END],
162
+ instruction='\n 十字キーで移動,[enter]で実行',
163
+ style=q.Style(
164
+ [
165
+ ('question', 'fg:#2196F3 bold'),
166
+ ('answer', 'fg:#FFB300 bold'),
167
+ ('pointer', 'fg:#FFB300 bold'),
168
+ ('highlighted', 'fg:#FFB300 bold'),
169
+ ('selected', 'fg:#FFB300 bold'),
170
+ ]
171
+ ),
172
+ ).ask()
173
+
174
+ 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
+ )
190
+ 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)
210
+
211
+ 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)
239
+
240
+ elif choice == END:
241
+ print('[bold red]終了します[/]')
242
+ else:
243
+ print('[bold red]無効な選択です[/]')
244
+
245
+
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)
243
249
  def download(
244
- first: Union[str, int, None] = None,
245
- second: Union[str, int, None] = None,
246
- base_path: str = '.',
250
+ first: Union[str, None] = None,
251
+ second: Union[str, None] = None,
252
+ base_path: str = '.',
247
253
  ) -> 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
- """難易度だけでなく, 問題番号も指定してコマンドを実行してください.
254
+ """
255
+ AtCoderの問題をダウンロードします
256
+
257
+ download
258
+ 対話形式でダウンロードを開始します。
259
+
260
+ download abc012
261
+ コンテスト abc012 の全問題をダウンロード
262
+
263
+ download A 120
264
+ 難易度Aの120番問題をダウンロード
265
+
266
+ download 120..130 B
267
+ ABCの120~130番のB問題をダウンロード
268
+
269
+ download 120
270
+ ABCの120番問題をダウンロード
271
+ """
272
+ if first is None:
273
+ interactive_download()
274
+ return
275
+
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
287
+
288
+ if are_all_diffs(first_args):
289
+ raise ValueError(
290
+ """難易度だけでなく, 問題番号も指定してコマンドを実行してください.
257
291
  例 atcdr -d A 120 : A問題の120をダウンロードます
258
292
  例 atcdr -d A 120..130 : A問題の120から130をダウンロードます
259
293
  """
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
- """次のような形式で問題を指定してください
294
+ )
295
+ else:
296
+ second_args = convert_arg(str(second))
297
+
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
+ """次のような形式で問題を指定してください
286
319
  例 atcdr -d A 120..130 : A問題の120から130をダウンロードします
287
320
  例 atcdr -d 120 : ABCのコンテストの問題をダウンロードします
288
321
  """
289
- )
322
+ )