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 +86 -0
- atcdr/download.py +275 -242
- atcdr/generate.py +180 -198
- atcdr/login.py +136 -0
- atcdr/logout.py +27 -0
- atcdr/markdown.py +34 -30
- atcdr/open.py +37 -32
- atcdr/submit.py +302 -0
- atcdr/test.py +365 -370
- atcdr/util/fileops.py +102 -0
- atcdr/util/filetype.py +71 -71
- atcdr/util/gpt.py +102 -96
- atcdr/util/parse.py +206 -0
- atcdr/util/problem.py +94 -91
- atcdr/util/session.py +140 -0
- atcoderstudybooster-0.3.2.dist-info/METADATA +213 -0
- atcoderstudybooster-0.3.2.dist-info/RECORD +21 -0
- {atcoderstudybooster-0.3.dist-info → atcoderstudybooster-0.3.2.dist-info}/WHEEL +1 -1
- atcoderstudybooster-0.3.2.dist-info/entry_points.txt +2 -0
- atcdr/main.py +0 -40
- atcdr/util/cost.py +0 -120
- atcdr/util/execute.py +0 -63
- atcoderstudybooster-0.3.dist-info/METADATA +0 -96
- atcoderstudybooster-0.3.dist-info/RECORD +0 -17
- atcoderstudybooster-0.3.dist-info/entry_points.txt +0 -2
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
|
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
|
10
|
-
from rich
|
11
|
-
from rich.prompt import
|
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.
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
88
|
+
base_path: str, problems: List[Problem], gene_path: Callable[[str, Problem], str]
|
101
89
|
) -> None:
|
102
|
-
|
103
|
-
|
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
|
-
|
106
|
-
|
107
|
-
continue
|
97
|
+
dir_path = gene_path(base_path, problem)
|
98
|
+
mkdir(dir_path)
|
108
99
|
|
109
|
-
|
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
|
-
|
102
|
+
title = problem_content.title or problem.label
|
103
|
+
title = title_to_filename(title)
|
115
104
|
|
116
|
-
|
117
|
-
|
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
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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(
|
127
|
-
|
128
|
-
|
129
|
-
|
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(
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
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
|
-
|
148
|
+
return all(isinstance(arg, Diff) for arg in args)
|
167
149
|
|
168
150
|
|
169
151
|
def interactive_download() -> None:
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
250
|
+
first: Union[str, None] = None,
|
251
|
+
second: Union[str, None] = None,
|
252
|
+
base_path: str = '.',
|
247
253
|
) -> None:
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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
|
+
)
|