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/util/fileops.py
ADDED
@@ -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
|
atcdr/util/filetype.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import os
|
2
2
|
from enum import Enum
|
3
|
-
from typing import Dict, List,
|
3
|
+
from typing import Dict, List, TypeAlias
|
4
4
|
|
5
5
|
# ファイル名と拡張子の型エイリアスを定義
|
6
6
|
Filename: TypeAlias = str
|
@@ -8,98 +8,98 @@ Extension: TypeAlias = str
|
|
8
8
|
|
9
9
|
|
10
10
|
class Lang(Enum):
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
PYTHON = 'Python'
|
12
|
+
JAVASCRIPT = 'JavaScript'
|
13
|
+
JAVA = 'Java'
|
14
|
+
C = 'C'
|
15
|
+
CPP = 'C++'
|
16
|
+
CSHARP = 'C#'
|
17
|
+
RUBY = 'Ruby'
|
18
|
+
PHP = 'php'
|
19
|
+
GO = 'Go'
|
20
|
+
RUST = 'Rust'
|
21
|
+
HTML = 'HTML'
|
22
|
+
MARKDOWN = 'markdown'
|
23
|
+
JSON = 'json'
|
24
24
|
|
25
25
|
|
26
26
|
# ファイル拡張子と対応する言語の辞書
|
27
27
|
FILE_EXTENSIONS: Dict[Lang, str] = {
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
28
|
+
Lang.PYTHON: '.py',
|
29
|
+
Lang.JAVASCRIPT: '.js',
|
30
|
+
Lang.JAVA: '.java',
|
31
|
+
Lang.C: '.c',
|
32
|
+
Lang.CPP: '.cpp',
|
33
|
+
Lang.CSHARP: '.cs',
|
34
|
+
Lang.RUBY: '.rb',
|
35
|
+
Lang.PHP: '.php',
|
36
|
+
Lang.GO: '.go',
|
37
|
+
Lang.RUST: '.rs',
|
38
|
+
Lang.HTML: '.html',
|
39
|
+
Lang.MARKDOWN: '.md',
|
40
|
+
Lang.JSON: '.json',
|
41
41
|
}
|
42
42
|
|
43
43
|
# ドキュメント言語のリスト
|
44
44
|
DOCUMENT_LANGUAGES: List[Lang] = [
|
45
|
-
|
46
|
-
|
47
|
-
|
45
|
+
Lang.HTML,
|
46
|
+
Lang.MARKDOWN,
|
47
|
+
Lang.JSON,
|
48
48
|
]
|
49
49
|
|
50
50
|
# コンパイル型言語のリスト
|
51
51
|
COMPILED_LANGUAGES: List[Lang] = [
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
52
|
+
Lang.JAVA,
|
53
|
+
Lang.C,
|
54
|
+
Lang.CPP,
|
55
|
+
Lang.CSHARP,
|
56
|
+
Lang.GO,
|
57
|
+
Lang.RUST,
|
58
58
|
]
|
59
59
|
|
60
60
|
# インタプリター型言語のリスト
|
61
61
|
INTERPRETED_LANGUAGES: List[Lang] = [
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
62
|
+
Lang.PYTHON,
|
63
|
+
Lang.JAVASCRIPT,
|
64
|
+
Lang.RUBY,
|
65
|
+
Lang.PHP,
|
66
66
|
]
|
67
67
|
|
68
68
|
|
69
69
|
def str2lang(lang: str) -> Lang:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
70
|
+
lang_map = {
|
71
|
+
'py': Lang.PYTHON,
|
72
|
+
'python': Lang.PYTHON,
|
73
|
+
'js': Lang.JAVASCRIPT,
|
74
|
+
'javascript': Lang.JAVASCRIPT,
|
75
|
+
'java': Lang.JAVA,
|
76
|
+
'c': Lang.C,
|
77
|
+
'cpp': Lang.CPP,
|
78
|
+
'c++': Lang.CPP,
|
79
|
+
'csharp': Lang.CSHARP,
|
80
|
+
'cs': Lang.CSHARP,
|
81
|
+
'c#': Lang.CSHARP,
|
82
|
+
'rb': Lang.RUBY,
|
83
|
+
'ruby': Lang.RUBY,
|
84
|
+
'php': Lang.PHP,
|
85
|
+
'go': Lang.GO,
|
86
|
+
'rs': Lang.RUST,
|
87
|
+
'rust': Lang.RUST,
|
88
|
+
'html': Lang.HTML,
|
89
|
+
'md': Lang.MARKDOWN,
|
90
|
+
'markdown': Lang.MARKDOWN,
|
91
|
+
'json': Lang.JSON,
|
92
|
+
}
|
93
|
+
return lang_map[lang.lower()]
|
94
94
|
|
95
95
|
|
96
96
|
def lang2str(lang: Lang) -> str:
|
97
|
-
|
97
|
+
return lang.value
|
98
98
|
|
99
99
|
|
100
|
-
def detect_language(path: str) ->
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
100
|
+
def detect_language(path: str) -> Lang:
|
101
|
+
ext = os.path.splitext(path)[1] # ファイルの拡張子を取得
|
102
|
+
lang = next(
|
103
|
+
(lang for lang, extension in FILE_EXTENSIONS.items() if extension == ext)
|
104
|
+
)
|
105
|
+
return lang
|
atcdr/util/gpt.py
CHANGED
@@ -1,112 +1,118 @@
|
|
1
1
|
import os
|
2
|
+
from enum import Enum
|
2
3
|
from typing import Dict, List, Optional
|
3
4
|
|
4
5
|
import requests
|
5
6
|
|
6
|
-
|
7
|
+
|
8
|
+
class Model(Enum):
|
9
|
+
GPT4O = 'gpt-4o'
|
10
|
+
GPT41 = 'gpt-4.1'
|
11
|
+
GPT41_MINI = 'gpt-4.1-mini'
|
12
|
+
GPT41_NANO = 'gpt-4.1-nano'
|
13
|
+
GPT4O_MINI = 'gpt-4o-mini'
|
14
|
+
O1_PREVIEW = 'o1-preview'
|
15
|
+
O1 = 'o1'
|
16
|
+
O3 = 'o3'
|
17
|
+
O1_MINI = 'o1-mini'
|
18
|
+
O3_MINI = 'o3-mini'
|
19
|
+
O4_MINI = 'o4-mini'
|
7
20
|
|
8
21
|
|
9
22
|
def set_api_key() -> Optional[str]:
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
23
|
+
api_key = os.getenv('OPENAI_API_KEY')
|
24
|
+
if api_key and validate_api_key(api_key):
|
25
|
+
return api_key
|
26
|
+
elif api_key:
|
27
|
+
print('環境変数に設定されているAPIキーの検証に失敗しました ')
|
28
|
+
else:
|
29
|
+
pass
|
30
|
+
|
31
|
+
api_key = input(
|
32
|
+
'https://platform.openai.com/api-keys からchatGPTのAPIキーを入手しましょう。\nAPIキー入力してください: '
|
33
|
+
)
|
34
|
+
if validate_api_key(api_key):
|
35
|
+
print('APIキーのテストに成功しました。')
|
36
|
+
print('以下, ~/.zshrcにAPIキーを保存しますか? [y/n]')
|
37
|
+
if input() == 'y':
|
38
|
+
zshrc_path = os.path.expanduser('~/.zshrc')
|
39
|
+
with open(zshrc_path, 'a') as f:
|
40
|
+
f.write(f'export OPENAI_API_KEY={api_key}\n')
|
41
|
+
print(
|
42
|
+
f'APIキーを {zshrc_path} に保存しました。次回シェル起動時に読み込まれます。'
|
43
|
+
)
|
44
|
+
os.environ['OPENAI_API_KEY'] = api_key
|
45
|
+
return api_key
|
46
|
+
else:
|
47
|
+
print('コード生成にはAPIキーが必要です。')
|
48
|
+
return None
|
36
49
|
|
37
50
|
|
38
51
|
def validate_api_key(api_key: str) -> bool:
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
52
|
+
headers = {
|
53
|
+
'Content-Type': 'application/json',
|
54
|
+
'Authorization': f'Bearer {api_key}',
|
55
|
+
}
|
43
56
|
|
44
|
-
|
57
|
+
response = requests.get('https://api.openai.com/v1/models', headers=headers)
|
45
58
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
if response.status_code == 200:
|
60
|
+
return True
|
61
|
+
else:
|
62
|
+
print('APIキーの検証に失敗しました。')
|
63
|
+
return False
|
51
64
|
|
52
65
|
|
53
66
|
class ChatGPT:
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
model=self.model, cost_type=CostType.INPUT, token_count=input_tokens
|
107
|
-
)
|
108
|
-
self.sum_cost += Rate.calc_cost(
|
109
|
-
model=self.model, cost_type=CostType.OUTPUT, token_count=output_tokens
|
110
|
-
)
|
111
|
-
|
112
|
-
return reply
|
67
|
+
API_URL = 'https://api.openai.com/v1/chat/completions'
|
68
|
+
|
69
|
+
# APIの使い方 https://platform.openai.com/docs/api-reference/making-requests
|
70
|
+
def __init__(
|
71
|
+
self,
|
72
|
+
api_key: Optional[str] = None,
|
73
|
+
model: Model = Model.GPT41_MINI,
|
74
|
+
max_tokens: int = 3000,
|
75
|
+
temperature: float = 0.7,
|
76
|
+
messages: Optional[List[Dict[str, str]]] = None,
|
77
|
+
system_prompt: str = 'You are a helpful assistant.',
|
78
|
+
) -> None:
|
79
|
+
self.api_key = api_key or os.getenv('OPENAI_API_KEY')
|
80
|
+
self.model = model
|
81
|
+
self.max_tokens = max_tokens
|
82
|
+
self.temperature = temperature
|
83
|
+
self.messages = (
|
84
|
+
messages
|
85
|
+
if messages is not None
|
86
|
+
else [{'role': 'system', 'content': system_prompt}]
|
87
|
+
)
|
88
|
+
|
89
|
+
self.__headers = {
|
90
|
+
'Content-Type': 'application/json',
|
91
|
+
'Authorization': f'Bearer {self.api_key}',
|
92
|
+
}
|
93
|
+
|
94
|
+
def tell(self, message: str) -> str:
|
95
|
+
self.messages.append({'role': 'user', 'content': message})
|
96
|
+
|
97
|
+
settings = {
|
98
|
+
'model': self.model.value,
|
99
|
+
'messages': self.messages,
|
100
|
+
'max_tokens': self.max_tokens,
|
101
|
+
'temperature': self.temperature,
|
102
|
+
}
|
103
|
+
|
104
|
+
response = requests.post(self.API_URL, headers=self.__headers, json=settings)
|
105
|
+
responsej = response.json()
|
106
|
+
try:
|
107
|
+
reply = responsej['choices'][0]['message']['content']
|
108
|
+
except KeyError:
|
109
|
+
print('Error:レスポンスの形式が正しくありません. \n' + str(responsej))
|
110
|
+
return 'Error: Unable to retrieve response.'
|
111
|
+
|
112
|
+
self.messages.append({'role': 'assistant', 'content': reply})
|
113
|
+
|
114
|
+
# usage = responsej['usage']
|
115
|
+
# input_tokens = usage.get('prompt_tokens', 0)
|
116
|
+
# output_tokens = usage.get('completion_tokens', 0)
|
117
|
+
|
118
|
+
return reply
|