AtCoderStudyBooster 0.3.1__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/PKG-INFO +11 -3
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/README.md +8 -1
- atcoderstudybooster-0.3.2/atcdr/cli.py +86 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/download.py +24 -2
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/generate.py +17 -26
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/login.py +3 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/logout.py +3 -0
- atcoderstudybooster-0.3.2/atcdr/markdown.py +43 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/open.py +8 -3
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/submit.py +12 -7
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/test.py +8 -7
- atcoderstudybooster-0.3.2/atcdr/util/fileops.py +102 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/pyproject.toml +4 -3
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/requirements-dev.lock +9 -5
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/requirements.lock +8 -4
- atcoderstudybooster-0.3.1/atcdr/main.py +0 -47
- atcoderstudybooster-0.3.1/atcdr/markdown.py +0 -39
- atcoderstudybooster-0.3.1/atcdr/util/execute.py +0 -63
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.github/workflows/deploy.yaml +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.gitignore +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.images/demo1.png +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.images/demo2.png +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.images/demo3.png +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.images/demo4.png +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.pre-commit-config.yaml +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.python-version +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.vscode/extensions.json +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.vscode/setting.json +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/.vscode/tasks.json +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/__init__.py +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/util/__init__.py +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/util/filetype.py +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/util/gpt.py +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/util/parse.py +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/util/problem.py +0 -0
- {atcoderstudybooster-0.3.1 → atcoderstudybooster-0.3.2}/atcdr/util/session.py +0 -0
@@ -1,17 +1,18 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: AtCoderStudyBooster
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.2
|
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>
|
7
7
|
License: MIT
|
8
8
|
Requires-Python: >=3.8
|
9
9
|
Requires-Dist: beautifulsoup4
|
10
|
-
Requires-Dist:
|
10
|
+
Requires-Dist: click-aliases>=1.0.5
|
11
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
|
15
|
+
Requires-Dist: rich-click>=1.8.8
|
15
16
|
Requires-Dist: rich>=13.7.1
|
16
17
|
Requires-Dist: tiktoken
|
17
18
|
Requires-Dist: types-beautifulsoup4>=4.12.0.20240511
|
@@ -22,14 +23,21 @@ Description-Content-Type: text/markdown
|
|
22
23
|
|
23
24
|
## 概要
|
24
25
|
|
25
|
-
|
26
|
+
🚧 このプロジェクトはまだ実験段階です。日々のAtCoder学習に役立つ機能を順次追加しています。
|
27
|
+
|
28
|
+
AtCoderStudyBoosterはAtCoderの学習を加速させるためのCLIツールです。問題をローカルにダウンロードし、テスト、提出、解答の作成をサポートするツールです。Pythonが入っていることが必須です。Pythonが入っている環境なら、`pip install AtCoderStudyBooster`でインストールできます。(Python3.8以上が必要です)
|
29
|
+
|
30
|
+
キャプチャ認証が導入されたあとでも、CLIからほぼ自動でログイン&提出できます。ただしキャプチャを解くために、GUI環境が必要になる場合があります。
|
26
31
|
|
27
32
|
このツールは以下のプロジェクトに強く影響を受けています。
|
28
33
|
[online-judge-tools](https://github.com/online-judge-tools)
|
29
34
|
[atcoder-cli](https://github.com/Tatamo/atcoder-cli)
|
30
35
|
|
36
|
+
|
31
37
|
## 利用ケース
|
32
38
|
|
39
|
+
まずは`download`コマンドを利用して問題をローカルにダウンロードしてみましょう。
|
40
|
+
|
33
41
|
### B問題の練習したい場合
|
34
42
|
|
35
43
|
ABCコンテストの223から226のB問題だけを集中的に練習したい場合、次のコマンドを実行します。
|
@@ -2,14 +2,21 @@
|
|
2
2
|
|
3
3
|
## 概要
|
4
4
|
|
5
|
-
|
5
|
+
🚧 このプロジェクトはまだ実験段階です。日々のAtCoder学習に役立つ機能を順次追加しています。
|
6
|
+
|
7
|
+
AtCoderStudyBoosterはAtCoderの学習を加速させるためのCLIツールです。問題をローカルにダウンロードし、テスト、提出、解答の作成をサポートするツールです。Pythonが入っていることが必須です。Pythonが入っている環境なら、`pip install AtCoderStudyBooster`でインストールできます。(Python3.8以上が必要です)
|
8
|
+
|
9
|
+
キャプチャ認証が導入されたあとでも、CLIからほぼ自動でログイン&提出できます。ただしキャプチャを解くために、GUI環境が必要になる場合があります。
|
6
10
|
|
7
11
|
このツールは以下のプロジェクトに強く影響を受けています。
|
8
12
|
[online-judge-tools](https://github.com/online-judge-tools)
|
9
13
|
[atcoder-cli](https://github.com/Tatamo/atcoder-cli)
|
10
14
|
|
15
|
+
|
11
16
|
## 利用ケース
|
12
17
|
|
18
|
+
まずは`download`コマンドを利用して問題をローカルにダウンロードしてみましょう。
|
19
|
+
|
13
20
|
### B問題の練習したい場合
|
14
21
|
|
15
22
|
ABCコンテストの223から226のB問題だけを集中的に練習したい場合、次のコマンドを実行します。
|
@@ -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()
|
@@ -4,6 +4,7 @@ import time
|
|
4
4
|
from typing import Callable, List, Union, cast
|
5
5
|
|
6
6
|
import questionary as q
|
7
|
+
import rich_click as click
|
7
8
|
from rich import print
|
8
9
|
from rich.prompt import Prompt
|
9
10
|
|
@@ -242,11 +243,32 @@ def interactive_download() -> None:
|
|
242
243
|
print('[bold red]無効な選択です[/]')
|
243
244
|
|
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)
|
245
249
|
def download(
|
246
|
-
first: Union[str,
|
247
|
-
second: Union[str,
|
250
|
+
first: Union[str, None] = None,
|
251
|
+
second: Union[str, None] = None,
|
248
252
|
base_path: str = '.',
|
249
253
|
) -> None:
|
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
|
+
"""
|
250
272
|
if first is None:
|
251
273
|
interactive_download()
|
252
274
|
return
|
@@ -2,12 +2,13 @@ import json
|
|
2
2
|
import os
|
3
3
|
import re
|
4
4
|
|
5
|
+
import rich_click as click
|
5
6
|
from rich.console import Console
|
6
7
|
from rich.panel import Panel
|
7
8
|
from rich.syntax import Syntax
|
8
9
|
|
9
10
|
from atcdr.test import ResultStatus, TestRunner, create_renderable_test_info
|
10
|
-
from atcdr.util.
|
11
|
+
from atcdr.util.fileops import add_file_selector
|
11
12
|
from atcdr.util.filetype import (
|
12
13
|
FILE_EXTENSIONS,
|
13
14
|
Filename,
|
@@ -181,31 +182,21 @@ Please provide an updated version of the code in {lang2str(lang)}."""
|
|
181
182
|
return
|
182
183
|
|
183
184
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
185
|
+
@click.command(short_help='コードを生成')
|
186
|
+
@add_file_selector('files', filetypes=[Lang.HTML])
|
187
|
+
@click.option('--lang', default='Python', help='出力するプログラミング言語')
|
188
|
+
@click.option('--model', default=Model.GPT41_MINI.value, help='使用するGPTモデル')
|
189
|
+
@click.option('--without-test', is_flag=True, help='テストケースを省略して生成')
|
190
|
+
@click.option('--template', is_flag=True, help='テンプレートを生成')
|
191
|
+
def generate(files, lang, model, without_test, template):
|
192
|
+
"""HTMLファイルからコード生成またはテンプレート出力を行います。"""
|
191
193
|
la = str2lang(lang)
|
192
194
|
model_enum = Model(model)
|
193
195
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
execute_files(
|
202
|
-
*source,
|
203
|
-
func=lambda file: generate_code(file, la, model_enum),
|
204
|
-
target_filetypes=[Lang.HTML],
|
205
|
-
)
|
206
|
-
else:
|
207
|
-
execute_files(
|
208
|
-
*source,
|
209
|
-
func=lambda file: solve_problem(file, la, model_enum),
|
210
|
-
target_filetypes=[Lang.HTML],
|
211
|
-
)
|
196
|
+
for path in files:
|
197
|
+
if template:
|
198
|
+
generate_template(path, la)
|
199
|
+
elif without_test:
|
200
|
+
generate_code(path, la, model_enum)
|
201
|
+
else:
|
202
|
+
solve_problem(path, la, model_enum)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import threading
|
2
2
|
import time
|
3
3
|
|
4
|
+
import rich_click as click
|
4
5
|
import webview
|
5
6
|
from requests import Session
|
6
7
|
from rich.console import Console
|
@@ -13,7 +14,9 @@ ATCODER_HOME_URL = 'https://atcoder.jp/home'
|
|
13
14
|
console = Console()
|
14
15
|
|
15
16
|
|
17
|
+
@click.command(short_help='AtCoderへログイン')
|
16
18
|
def login() -> None:
|
19
|
+
"""AtCoderへログインします."""
|
17
20
|
session = load_session()
|
18
21
|
if validate_session(session):
|
19
22
|
console.print('[green][+][/] すでにログインしています. ')
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import rich_click as click
|
1
2
|
import webview
|
2
3
|
from rich import print
|
3
4
|
|
@@ -6,7 +7,9 @@ from atcdr.util.session import delete_session, load_session, validate_session
|
|
6
7
|
ATCODER_LOGIN_URL = 'https://atcoder.jp/login'
|
7
8
|
|
8
9
|
|
10
|
+
@click.command(short_help='AtCoderへログアウト')
|
9
11
|
def logout() -> None:
|
12
|
+
"""AtCoderからログアウトします."""
|
10
13
|
session = load_session()
|
11
14
|
if not validate_session(session):
|
12
15
|
print('[red][-][/] ログインしていません.')
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import rich_click as click
|
4
|
+
from rich.console import Console
|
5
|
+
from rich.markdown import Markdown
|
6
|
+
|
7
|
+
from atcdr.util.fileops import add_file_selector
|
8
|
+
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
9
|
+
from atcdr.util.parse import ProblemHTML
|
10
|
+
|
11
|
+
|
12
|
+
def save_markdown(html_path: str, lang: str) -> None:
|
13
|
+
console = Console()
|
14
|
+
with open(html_path, 'r', encoding='utf-8') as f:
|
15
|
+
html = ProblemHTML(f.read())
|
16
|
+
md = html.make_problem_markdown(lang)
|
17
|
+
file_without_ext = os.path.splitext(html_path)[0]
|
18
|
+
md_path = file_without_ext + FILE_EXTENSIONS[Lang.MARKDOWN]
|
19
|
+
|
20
|
+
with open(md_path, 'w', encoding='utf-8') as f:
|
21
|
+
f.write(md)
|
22
|
+
console.print('[green][+][/green] Markdownファイルを作成しました.')
|
23
|
+
|
24
|
+
|
25
|
+
def print_markdown(html_path: str, lang: str) -> None:
|
26
|
+
console = Console()
|
27
|
+
with open(html_path, 'r', encoding='utf-8') as f:
|
28
|
+
html = ProblemHTML(f.read())
|
29
|
+
md = html.make_problem_markdown(lang)
|
30
|
+
console.print(Markdown(md))
|
31
|
+
|
32
|
+
|
33
|
+
@click.command(short_help='Markdown形式で問題を表示します')
|
34
|
+
@add_file_selector('files', filetypes=[Lang.HTML])
|
35
|
+
@click.option('--lang', default='ja', help='出力する言語を指定')
|
36
|
+
@click.option('--save', is_flag=True, help='変換結果をファイルに保存')
|
37
|
+
def markdown(files, lang, save):
|
38
|
+
"""Markdown形式で問題を表示します"""
|
39
|
+
for path in files:
|
40
|
+
if save:
|
41
|
+
save_markdown(path, lang)
|
42
|
+
else:
|
43
|
+
print_markdown(path, lang)
|
@@ -3,7 +3,8 @@ from rich.panel import Panel
|
|
3
3
|
from rich.console import Console
|
4
4
|
|
5
5
|
from atcdr.util.filetype import Lang
|
6
|
-
from atcdr.util.
|
6
|
+
from atcdr.util.fileops import add_file_selector
|
7
|
+
import rich_click as click
|
7
8
|
from atcdr.util.parse import ProblemHTML
|
8
9
|
|
9
10
|
|
@@ -39,5 +40,9 @@ def open_html(file: str) -> None:
|
|
39
40
|
)
|
40
41
|
|
41
42
|
|
42
|
-
|
43
|
-
|
43
|
+
@click.command(short_help='HTMLファイルを開く')
|
44
|
+
@add_file_selector('files', filetypes=[Lang.HTML])
|
45
|
+
def open_files(files):
|
46
|
+
"""指定したHTMLファイルをブラウザで開きます。"""
|
47
|
+
for path in files:
|
48
|
+
open_html(path)
|
@@ -5,6 +5,7 @@ from typing import Dict, List, NamedTuple, Optional
|
|
5
5
|
|
6
6
|
import questionary as q
|
7
7
|
import requests
|
8
|
+
import rich_click as click
|
8
9
|
import webview
|
9
10
|
from bs4 import BeautifulSoup as bs
|
10
11
|
from rich import print
|
@@ -19,7 +20,7 @@ from atcdr.test import (
|
|
19
20
|
TestRunner,
|
20
21
|
create_renderable_test_info,
|
21
22
|
)
|
22
|
-
from atcdr.util.
|
23
|
+
from atcdr.util.fileops import add_file_selector
|
23
24
|
from atcdr.util.filetype import (
|
24
25
|
COMPILED_LANGUAGES,
|
25
26
|
INTERPRETED_LANGUAGES,
|
@@ -289,9 +290,13 @@ def submit_source(path: str, no_test: bool, no_feedback: bool) -> None:
|
|
289
290
|
print_status_submission(api_status_link, path, session)
|
290
291
|
|
291
292
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
293
|
+
@click.command(short_help='ソースを提出')
|
294
|
+
@add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
|
295
|
+
@click.option('--no-test', is_flag=True, default=False, help='テストをスキップ')
|
296
|
+
@click.option(
|
297
|
+
'--no-feedback', is_flag=True, default=False, help='フィードバックをスキップ'
|
298
|
+
)
|
299
|
+
def submit(files, no_test, no_feedback):
|
300
|
+
"""指定したファイルをAtCoderへ提出します。"""
|
301
|
+
for path in files:
|
302
|
+
submit_source(path, no_test, no_feedback)
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
|
|
6
6
|
from enum import Enum
|
7
7
|
from typing import Dict, List, Optional, Tuple, Union
|
8
8
|
|
9
|
+
import rich_click as click
|
9
10
|
from rich.console import Group, RenderableType
|
10
11
|
from rich.live import Live
|
11
12
|
from rich.markup import escape
|
@@ -17,7 +18,7 @@ from rich.syntax import Syntax
|
|
17
18
|
from rich.table import Table
|
18
19
|
from rich.text import Text
|
19
20
|
|
20
|
-
from atcdr.util.
|
21
|
+
from atcdr.util.fileops import add_file_selector
|
21
22
|
from atcdr.util.filetype import (
|
22
23
|
COMPILED_LANGUAGES,
|
23
24
|
INTERPRETED_LANGUAGES,
|
@@ -424,9 +425,9 @@ def run_test(path_of_code: str) -> None:
|
|
424
425
|
render_results(test)
|
425
426
|
|
426
427
|
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
428
|
+
@click.command(short_help='テストを実行')
|
429
|
+
@add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
|
430
|
+
def test(files):
|
431
|
+
"""指定したソースコードをサンプルケースでテストします。"""
|
432
|
+
for path in files:
|
433
|
+
run_test(path)
|
@@ -0,0 +1,102 @@
|
|
1
|
+
import functools
|
2
|
+
import glob
|
3
|
+
import os
|
4
|
+
from typing import List, Tuple
|
5
|
+
|
6
|
+
import questionary as q
|
7
|
+
import rich_click as click
|
8
|
+
|
9
|
+
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
10
|
+
|
11
|
+
|
12
|
+
def collect_files(
|
13
|
+
patterns: Tuple[str, ...],
|
14
|
+
exts: Tuple[str, ...],
|
15
|
+
recursive: bool,
|
16
|
+
) -> List[str]:
|
17
|
+
# 1) ベース候補
|
18
|
+
if recursive:
|
19
|
+
candidates = []
|
20
|
+
for root, _, files in os.walk('.'):
|
21
|
+
for f in files:
|
22
|
+
candidates.append(os.path.join(root, f))
|
23
|
+
else:
|
24
|
+
candidates = [f for f in os.listdir('.') if os.path.isfile(f)]
|
25
|
+
|
26
|
+
# 2) パターン+glob 展開
|
27
|
+
matched = set()
|
28
|
+
pats = patterns or ['*']
|
29
|
+
for pat in pats:
|
30
|
+
for m in glob.glob(pat, recursive=recursive):
|
31
|
+
if os.path.isfile(m):
|
32
|
+
matched.add(m)
|
33
|
+
if '*' not in pat and os.path.isfile(pat):
|
34
|
+
matched.add(pat)
|
35
|
+
|
36
|
+
if exts:
|
37
|
+
matched = {f for f in matched if os.path.splitext(f)[1] in exts}
|
38
|
+
|
39
|
+
return sorted(matched)
|
40
|
+
|
41
|
+
|
42
|
+
def select_files_interactively(files: List[str]) -> List[str]:
|
43
|
+
target_file = q.select(
|
44
|
+
message='複数のファイルが見つかりました.ファイルを選択してください:',
|
45
|
+
choices=[q.Choice(title=file, value=file) for file in files],
|
46
|
+
instruction='\n 十字キーで移動, [enter]で実行',
|
47
|
+
pointer='❯',
|
48
|
+
qmark='',
|
49
|
+
style=q.Style(
|
50
|
+
[
|
51
|
+
('qmark', 'fg:#2196F3 bold'),
|
52
|
+
('question', 'fg:#2196F3 bold'),
|
53
|
+
('answer', 'fg:#FFB300 bold'),
|
54
|
+
('pointer', 'fg:#FFB300 bold'),
|
55
|
+
('highlighted', 'fg:#FFB300 bold'),
|
56
|
+
('selected', 'fg:#FFB300 bold'),
|
57
|
+
]
|
58
|
+
),
|
59
|
+
).ask()
|
60
|
+
return target_file
|
61
|
+
|
62
|
+
|
63
|
+
def add_file_selector(
|
64
|
+
arg_name: str,
|
65
|
+
filetypes: list[Lang],
|
66
|
+
):
|
67
|
+
def decorator(f):
|
68
|
+
@click.argument(arg_name, nargs=-1, type=click.STRING)
|
69
|
+
@click.pass_context
|
70
|
+
@functools.wraps(f)
|
71
|
+
def wrapper(ctx: click.Context, **kwargs):
|
72
|
+
# Click から渡される元のパターン一覧を取得
|
73
|
+
patterns: tuple[str, ...] = kwargs.pop(arg_name)
|
74
|
+
|
75
|
+
# 1) 拡張子リストを作成
|
76
|
+
exts = [FILE_EXTENSIONS[lang] for lang in filetypes]
|
77
|
+
|
78
|
+
# 2) ファイル収集 (非再帰固定)
|
79
|
+
files = collect_files(patterns, tuple(exts), recursive=False)
|
80
|
+
if not files:
|
81
|
+
click.echo('対象ファイルが見つかりません。')
|
82
|
+
ctx.exit(1)
|
83
|
+
|
84
|
+
# 3) 候補が1つなら即実行
|
85
|
+
if len(files) == 1:
|
86
|
+
return ctx.invoke(f, **{arg_name: files}, **kwargs)
|
87
|
+
|
88
|
+
# 4) 引数なしなら対話選択、それ以外はまとめて渡す
|
89
|
+
if not patterns:
|
90
|
+
selected = select_files_interactively(files)
|
91
|
+
if not selected:
|
92
|
+
click.echo('ファイルが選択されませんでした。')
|
93
|
+
ctx.exit(1)
|
94
|
+
selected_list = [selected]
|
95
|
+
return ctx.invoke(f, **{arg_name: selected_list}, **kwargs)
|
96
|
+
|
97
|
+
# 5) patterns 指定ありならマッチ全件を渡す
|
98
|
+
return ctx.invoke(f, **{arg_name: files}, **kwargs)
|
99
|
+
|
100
|
+
return wrapper
|
101
|
+
|
102
|
+
return decorator
|
@@ -1,13 +1,12 @@
|
|
1
1
|
[project]
|
2
2
|
name = "AtCoderStudyBooster"
|
3
|
-
version = "0.3.
|
3
|
+
version = "0.3.2"
|
4
4
|
description = "A tool to download and manage AtCoder problems."
|
5
5
|
authors = [
|
6
6
|
{ name = "yuta6", email = "46110512+yuta6@users.noreply.github.com" }
|
7
7
|
]
|
8
8
|
dependencies = [
|
9
9
|
"beautifulsoup4",
|
10
|
-
"fire",
|
11
10
|
"requests",
|
12
11
|
"tiktoken",
|
13
12
|
"types-requests>=2.32.0.20240712",
|
@@ -16,6 +15,8 @@ dependencies = [
|
|
16
15
|
"rich>=13.7.1",
|
17
16
|
"questionary>=2.0.1",
|
18
17
|
"pywebview>=5.4",
|
18
|
+
"rich-click>=1.8.8",
|
19
|
+
"click-aliases>=1.0.5",
|
19
20
|
]
|
20
21
|
readme = "README.md"
|
21
22
|
requires-python = ">= 3.8"
|
@@ -23,7 +24,7 @@ license = { text = "MIT" }
|
|
23
24
|
urls = { Homepage = "https://github.com/yuta6/AtCoderStudyBooster" }
|
24
25
|
|
25
26
|
[project.scripts]
|
26
|
-
"atcdr" = "atcdr.
|
27
|
+
"atcdr" = "atcdr.cli:cli"
|
27
28
|
|
28
29
|
[build-system]
|
29
30
|
requires = ["hatchling==1.26.3"]
|
@@ -21,12 +21,15 @@ cfgv==3.4.0
|
|
21
21
|
# via pre-commit
|
22
22
|
charset-normalizer==3.3.2
|
23
23
|
# via requests
|
24
|
+
click==8.1.8
|
25
|
+
# via click-aliases
|
26
|
+
# via rich-click
|
27
|
+
click-aliases==1.0.5
|
28
|
+
# via atcoderstudybooster
|
24
29
|
distlib==0.3.8
|
25
30
|
# via virtualenv
|
26
31
|
filelock==3.15.4
|
27
32
|
# via virtualenv
|
28
|
-
fire==0.6.0
|
29
|
-
# via atcoderstudybooster
|
30
33
|
identify==2.6.0
|
31
34
|
# via pre-commit
|
32
35
|
idna==3.7
|
@@ -71,13 +74,13 @@ requests==2.32.3
|
|
71
74
|
# via tiktoken
|
72
75
|
rich==13.7.1
|
73
76
|
# via atcoderstudybooster
|
77
|
+
# via rich-click
|
78
|
+
rich-click==1.8.8
|
79
|
+
# via atcoderstudybooster
|
74
80
|
six==1.16.0
|
75
|
-
# via fire
|
76
81
|
# via markdownify
|
77
82
|
soupsieve==2.5
|
78
83
|
# via beautifulsoup4
|
79
|
-
termcolor==2.4.0
|
80
|
-
# via fire
|
81
84
|
tiktoken==0.7.0
|
82
85
|
# via atcoderstudybooster
|
83
86
|
types-beautifulsoup4==4.12.0.20240511
|
@@ -89,6 +92,7 @@ types-requests==2.32.0.20240712
|
|
89
92
|
typing-extensions==4.12.2
|
90
93
|
# via mypy
|
91
94
|
# via pywebview
|
95
|
+
# via rich-click
|
92
96
|
urllib3==2.2.2
|
93
97
|
# via requests
|
94
98
|
# via types-requests
|
@@ -19,7 +19,10 @@ certifi==2024.7.4
|
|
19
19
|
# via requests
|
20
20
|
charset-normalizer==3.3.2
|
21
21
|
# via requests
|
22
|
-
|
22
|
+
click==8.1.8
|
23
|
+
# via click-aliases
|
24
|
+
# via rich-click
|
25
|
+
click-aliases==1.0.5
|
23
26
|
# via atcoderstudybooster
|
24
27
|
idna==3.7
|
25
28
|
# via requests
|
@@ -46,13 +49,13 @@ requests==2.32.3
|
|
46
49
|
# via tiktoken
|
47
50
|
rich==13.7.1
|
48
51
|
# via atcoderstudybooster
|
52
|
+
# via rich-click
|
53
|
+
rich-click==1.8.8
|
54
|
+
# via atcoderstudybooster
|
49
55
|
six==1.16.0
|
50
|
-
# via fire
|
51
56
|
# via markdownify
|
52
57
|
soupsieve==2.5
|
53
58
|
# via beautifulsoup4
|
54
|
-
termcolor==2.4.0
|
55
|
-
# via fire
|
56
59
|
tiktoken==0.7.0
|
57
60
|
# via atcoderstudybooster
|
58
61
|
types-beautifulsoup4==4.12.0.20240511
|
@@ -63,6 +66,7 @@ types-requests==2.32.0.20240712
|
|
63
66
|
# via atcoderstudybooster
|
64
67
|
typing-extensions==4.13.2
|
65
68
|
# via pywebview
|
69
|
+
# via rich-click
|
66
70
|
urllib3==2.2.2
|
67
71
|
# via requests
|
68
72
|
# via types-requests
|
@@ -1,47 +0,0 @@
|
|
1
|
-
from importlib.metadata import metadata
|
2
|
-
|
3
|
-
import fire # type: ignore
|
4
|
-
from rich.traceback import install
|
5
|
-
|
6
|
-
from atcdr.download import download
|
7
|
-
from atcdr.generate import generate
|
8
|
-
from atcdr.login import login
|
9
|
-
from atcdr.logout import logout
|
10
|
-
from atcdr.markdown import markdown
|
11
|
-
from atcdr.open import open_files
|
12
|
-
from atcdr.submit import submit
|
13
|
-
from atcdr.test import test
|
14
|
-
|
15
|
-
|
16
|
-
def get_version() -> None:
|
17
|
-
meta = metadata('AtCoderStudyBooster')
|
18
|
-
print(meta['Name'], meta['Version'])
|
19
|
-
|
20
|
-
|
21
|
-
MAP_COMMANDS: dict = {
|
22
|
-
'test': test,
|
23
|
-
't': test,
|
24
|
-
'download': download,
|
25
|
-
'd': download,
|
26
|
-
'open': open_files,
|
27
|
-
'o': open_files,
|
28
|
-
'generate': generate,
|
29
|
-
'g': generate,
|
30
|
-
'markdown': markdown,
|
31
|
-
'md': markdown,
|
32
|
-
'login': login,
|
33
|
-
'logout': logout,
|
34
|
-
'submit': submit,
|
35
|
-
's': submit,
|
36
|
-
'--version': get_version,
|
37
|
-
'-v': get_version,
|
38
|
-
}
|
39
|
-
|
40
|
-
|
41
|
-
def main():
|
42
|
-
install()
|
43
|
-
fire.Fire(MAP_COMMANDS)
|
44
|
-
|
45
|
-
|
46
|
-
if __name__ == '__main__':
|
47
|
-
main()
|
@@ -1,39 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
|
3
|
-
from rich.console import Console
|
4
|
-
from rich.markdown import Markdown
|
5
|
-
|
6
|
-
from atcdr.util.execute import execute_files
|
7
|
-
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
8
|
-
from atcdr.util.parse import ProblemHTML
|
9
|
-
|
10
|
-
|
11
|
-
def save_markdown(html_path: str, lang: str) -> None:
|
12
|
-
console = Console()
|
13
|
-
with open(html_path, 'r', encoding='utf-8') as f:
|
14
|
-
html = ProblemHTML(f.read())
|
15
|
-
md = html.make_problem_markdown(lang)
|
16
|
-
file_without_ext = os.path.splitext(html_path)[0]
|
17
|
-
md_path = file_without_ext + FILE_EXTENSIONS[Lang.MARKDOWN]
|
18
|
-
|
19
|
-
with open(md_path, 'w', encoding='utf-8') as f:
|
20
|
-
f.write(md)
|
21
|
-
console.print('[green][+][/green] Markdownファイルを作成しました.')
|
22
|
-
|
23
|
-
|
24
|
-
def print_markdown(md_path: str) -> None:
|
25
|
-
console = Console()
|
26
|
-
with open(md_path, 'r', encoding='utf-8') as f:
|
27
|
-
md = f.read()
|
28
|
-
console.print(Markdown(md))
|
29
|
-
|
30
|
-
|
31
|
-
def markdown(*args: str, lang: str = 'ja', save: bool = False) -> None:
|
32
|
-
if save:
|
33
|
-
execute_files(
|
34
|
-
*args,
|
35
|
-
func=lambda html_path: save_markdown(html_path, lang),
|
36
|
-
target_filetypes=[Lang.HTML],
|
37
|
-
)
|
38
|
-
else:
|
39
|
-
execute_files(*args, func=print_markdown, target_filetypes=[Lang.MARKDOWN])
|
@@ -1,63 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
from typing import Callable, List
|
3
|
-
|
4
|
-
import questionary as q
|
5
|
-
from rich import print
|
6
|
-
|
7
|
-
from atcdr.util.filetype import FILE_EXTENSIONS, Filename, Lang
|
8
|
-
|
9
|
-
|
10
|
-
def execute_files(
|
11
|
-
*args: str, func: Callable[[Filename], None], target_filetypes: List[Lang]
|
12
|
-
) -> None:
|
13
|
-
target_extensions = [FILE_EXTENSIONS[lang] for lang in target_filetypes]
|
14
|
-
|
15
|
-
files = [
|
16
|
-
file
|
17
|
-
for file in os.listdir('.')
|
18
|
-
if os.path.isfile(file) and os.path.splitext(file)[1] in target_extensions
|
19
|
-
]
|
20
|
-
|
21
|
-
if not files:
|
22
|
-
print(
|
23
|
-
'対象のファイルが見つかりません.\n対象ファイルが存在するディレクトリーに移動してから実行してください。'
|
24
|
-
)
|
25
|
-
return
|
26
|
-
|
27
|
-
if not args:
|
28
|
-
if len(files) == 1:
|
29
|
-
func(files[0])
|
30
|
-
else:
|
31
|
-
target_file = q.select(
|
32
|
-
message='複数のファイルが見つかりました.ファイルを選択してください:',
|
33
|
-
choices=[q.Choice(title=file, value=file) for file in files],
|
34
|
-
instruction='\n 十字キーで移動, [enter]で実行',
|
35
|
-
pointer='❯❯❯',
|
36
|
-
qmark='',
|
37
|
-
style=q.Style(
|
38
|
-
[
|
39
|
-
('qmark', 'fg:#2196F3 bold'),
|
40
|
-
('question', 'fg:#2196F3 bold'),
|
41
|
-
('answer', 'fg:#FFB300 bold'),
|
42
|
-
('pointer', 'fg:#FFB300 bold'),
|
43
|
-
('highlighted', 'fg:#FFB300 bold'),
|
44
|
-
('selected', 'fg:#FFB300 bold'),
|
45
|
-
]
|
46
|
-
),
|
47
|
-
).ask()
|
48
|
-
list(map(func, [target_file]))
|
49
|
-
else:
|
50
|
-
target_files = set()
|
51
|
-
for arg in args:
|
52
|
-
if arg == '*':
|
53
|
-
target_files.update(files)
|
54
|
-
elif arg.startswith('*.'):
|
55
|
-
ext = arg[1:] # ".py" のような拡張子を取得
|
56
|
-
target_files.update(file for file in files if file.endswith(ext))
|
57
|
-
else:
|
58
|
-
if arg in files:
|
59
|
-
target_files.add(arg)
|
60
|
-
else:
|
61
|
-
print(f'エラー: {arg} は存在しません。')
|
62
|
-
|
63
|
-
list(map(func, target_files))
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|