AtCoderStudyBooster 0.4.0__py3-none-any.whl → 0.4.1__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 +32 -27
- atcdr/generate.py +19 -24
- atcdr/login.py +10 -11
- atcdr/logout.py +4 -3
- atcdr/markdown.py +9 -5
- atcdr/open.py +5 -4
- atcdr/submit.py +21 -24
- atcdr/test.py +21 -16
- atcdr/util/fileops.py +5 -4
- atcdr/util/gpt.py +10 -12
- atcdr/util/i18n.py +317 -0
- atcdr/util/parse.py +16 -11
- atcdr/util/problem.py +3 -2
- atcdr/util/session.py +15 -14
- atcoderstudybooster-0.4.1.dist-info/METADATA +248 -0
- atcoderstudybooster-0.4.1.dist-info/RECORD +22 -0
- atcoderstudybooster-0.4.0.dist-info/METADATA +0 -261
- atcoderstudybooster-0.4.0.dist-info/RECORD +0 -21
- {atcoderstudybooster-0.4.0.dist-info → atcoderstudybooster-0.4.1.dist-info}/WHEEL +0 -0
- {atcoderstudybooster-0.4.0.dist-info → atcoderstudybooster-0.4.1.dist-info}/entry_points.txt +0 -0
atcdr/test.py
CHANGED
@@ -26,6 +26,7 @@ from atcdr.util.filetype import (
|
|
26
26
|
detect_language,
|
27
27
|
lang2str,
|
28
28
|
)
|
29
|
+
from atcdr.util.i18n import _
|
29
30
|
from atcdr.util.parse import ProblemHTML
|
30
31
|
|
31
32
|
|
@@ -159,7 +160,7 @@ class TestRunner:
|
|
159
160
|
]
|
160
161
|
self.exe = None
|
161
162
|
else:
|
162
|
-
raise ValueError(
|
163
|
+
raise ValueError(_('runner_not_found', lang))
|
163
164
|
|
164
165
|
return self
|
165
166
|
|
@@ -300,9 +301,9 @@ def create_renderable_test_info(
|
|
300
301
|
status_text = STATUS_TEXT_MAP[test_info.summary]
|
301
302
|
|
302
303
|
header_text = Text.assemble(
|
303
|
-
Text.from_markup(f'[cyan]{test_info.sourcename}[/]
|
304
|
+
Text.from_markup(f'[cyan]{test_info.sourcename}[/]' + _('test_of', '') + '\n'),
|
304
305
|
Text.from_markup(
|
305
|
-
|
306
|
+
'[italic #0f0f0f]' + _('compile_time', test_info.compile_time) + '\n'
|
306
307
|
)
|
307
308
|
if test_info.compile_time
|
308
309
|
else Text(''),
|
@@ -319,7 +320,7 @@ def create_renderable_test_info(
|
|
319
320
|
|
320
321
|
if test_info.compiler_message:
|
321
322
|
rule = Rule(
|
322
|
-
title='
|
323
|
+
title=_('compiler_message'),
|
323
324
|
style=COLOR_MAP[ResultStatus.CE],
|
324
325
|
)
|
325
326
|
components.append(rule)
|
@@ -342,27 +343,29 @@ def create_renderable_test_result(
|
|
342
343
|
|
343
344
|
# 以下の部分は if-else ブロックの外に移動
|
344
345
|
status_header = Text.assemble(
|
345
|
-
'
|
346
|
+
_('status'),
|
346
347
|
STATUS_TEXT_MAP[test_result.result.passed], # status_text をここに追加
|
347
348
|
)
|
348
349
|
|
349
350
|
execution_time_text = None
|
350
351
|
if test_result.result.executed_time is not None:
|
351
352
|
execution_time_text = Text.from_markup(
|
352
|
-
|
353
|
+
_('execution_time', test_result.result.executed_time)
|
353
354
|
)
|
354
355
|
|
355
356
|
table = Table(show_header=True, header_style='bold')
|
356
|
-
table.add_column('
|
357
|
+
table.add_column(_('input'), style='cyan', min_width=10)
|
357
358
|
|
358
359
|
if test_result.result.passed != ResultStatus.AC:
|
359
360
|
table.add_column(
|
360
|
-
'
|
361
|
+
_('output'),
|
361
362
|
style=COLOR_MAP[test_result.result.passed],
|
362
363
|
min_width=10,
|
363
364
|
overflow='fold',
|
364
365
|
)
|
365
|
-
table.add_column(
|
366
|
+
table.add_column(
|
367
|
+
_('expected_output'), style=COLOR_MAP[ResultStatus.AC], min_width=10
|
368
|
+
)
|
366
369
|
table.add_row(
|
367
370
|
escape(test_result.testcase.input),
|
368
371
|
escape(test_result.result.output),
|
@@ -370,7 +373,7 @@ def create_renderable_test_result(
|
|
370
373
|
)
|
371
374
|
else:
|
372
375
|
table.add_column(
|
373
|
-
'
|
376
|
+
_('output'), style=COLOR_MAP[test_result.result.passed], min_width=10
|
374
377
|
)
|
375
378
|
table.add_row(
|
376
379
|
escape(test_result.testcase.input), escape(test_result.result.output)
|
@@ -393,7 +396,9 @@ def render_results(test: TestRunner) -> None:
|
|
393
396
|
SpinnerColumn(style='white', spinner_name='simpleDots'),
|
394
397
|
BarColumn(),
|
395
398
|
)
|
396
|
-
task_id = progress.add_task(
|
399
|
+
task_id = progress.add_task(
|
400
|
+
description=_('test_in_progress'), total=test.info.case_number
|
401
|
+
)
|
397
402
|
|
398
403
|
current_display = [create_renderable_test_info(test.info, progress)]
|
399
404
|
|
@@ -404,7 +409,9 @@ def render_results(test: TestRunner) -> None:
|
|
404
409
|
current_display.insert(-1, (create_renderable_test_result(i, result)))
|
405
410
|
live.update(Group(*current_display))
|
406
411
|
|
407
|
-
progress.update(
|
412
|
+
progress.update(
|
413
|
+
task_id, description=_('test_completed')
|
414
|
+
) # 完了メッセージに更新
|
408
415
|
current_display[-1] = create_renderable_test_info(test.info, progress)
|
409
416
|
live.update(Group(*current_display))
|
410
417
|
|
@@ -412,9 +419,7 @@ def render_results(test: TestRunner) -> None:
|
|
412
419
|
def run_test(path_of_code: str) -> None:
|
413
420
|
html_paths = [f for f in os.listdir('.') if f.endswith('.html')]
|
414
421
|
if not html_paths:
|
415
|
-
print(
|
416
|
-
'問題のファイルが見つかりません。\n問題のファイルが存在するディレクトリーに移動してから実行してください。'
|
417
|
-
)
|
422
|
+
print(_('problem_file_not_found'))
|
418
423
|
return
|
419
424
|
|
420
425
|
with open(html_paths[0], 'r') as file:
|
@@ -425,7 +430,7 @@ def run_test(path_of_code: str) -> None:
|
|
425
430
|
render_results(test)
|
426
431
|
|
427
432
|
|
428
|
-
@click.command(short_help='
|
433
|
+
@click.command(short_help=_('cmd_test'), help=_('cmd_test'))
|
429
434
|
@add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
|
430
435
|
def test(files):
|
431
436
|
"""指定したソースコードをサンプルケースでテストします。"""
|
atcdr/util/fileops.py
CHANGED
@@ -7,6 +7,7 @@ import questionary as q
|
|
7
7
|
import rich_click as click
|
8
8
|
|
9
9
|
from atcdr.util.filetype import FILE_EXTENSIONS, Lang
|
10
|
+
from atcdr.util.i18n import _
|
10
11
|
|
11
12
|
|
12
13
|
def collect_files(
|
@@ -41,9 +42,9 @@ def collect_files(
|
|
41
42
|
|
42
43
|
def select_files_interactively(files: List[str]) -> List[str]:
|
43
44
|
target_file = q.select(
|
44
|
-
message='
|
45
|
+
message=_('multiple_files_found'),
|
45
46
|
choices=[q.Choice(title=file, value=file) for file in files],
|
46
|
-
instruction='\n
|
47
|
+
instruction='\n ' + _('navigate_with_arrows'),
|
47
48
|
pointer='❯',
|
48
49
|
qmark='',
|
49
50
|
style=q.Style(
|
@@ -78,7 +79,7 @@ def add_file_selector(
|
|
78
79
|
# 2) ファイル収集 (非再帰固定)
|
79
80
|
files = collect_files(patterns, tuple(exts), recursive=False)
|
80
81
|
if not files:
|
81
|
-
click.echo('
|
82
|
+
click.echo(_('target_file_not_found'))
|
82
83
|
ctx.exit(1)
|
83
84
|
|
84
85
|
# 3) 候補が1つなら即実行
|
@@ -89,7 +90,7 @@ def add_file_selector(
|
|
89
90
|
if not patterns:
|
90
91
|
selected = select_files_interactively(files)
|
91
92
|
if not selected:
|
92
|
-
click.echo('
|
93
|
+
click.echo(_('file_not_selected'))
|
93
94
|
ctx.exit(1)
|
94
95
|
selected_list = [selected]
|
95
96
|
return ctx.invoke(f, **{arg_name: selected_list}, **kwargs)
|
atcdr/util/gpt.py
CHANGED
@@ -4,6 +4,8 @@ from typing import Dict, List, Optional
|
|
4
4
|
|
5
5
|
import requests
|
6
6
|
|
7
|
+
from atcdr.util.i18n import _
|
8
|
+
|
7
9
|
|
8
10
|
class Model(Enum):
|
9
11
|
GPT4O = 'gpt-4o'
|
@@ -24,27 +26,23 @@ def set_api_key() -> Optional[str]:
|
|
24
26
|
if api_key and validate_api_key(api_key):
|
25
27
|
return api_key
|
26
28
|
elif api_key:
|
27
|
-
print('
|
29
|
+
print(_('api_key_validation_failed'))
|
28
30
|
else:
|
29
31
|
pass
|
30
32
|
|
31
|
-
api_key = input(
|
32
|
-
'https://platform.openai.com/api-keys からchatGPTのAPIキーを入手しましょう。\nAPIキー入力してください: '
|
33
|
-
)
|
33
|
+
api_key = input(_('get_api_key_prompt'))
|
34
34
|
if validate_api_key(api_key):
|
35
|
-
print('
|
36
|
-
print('
|
35
|
+
print(_('api_key_test_success'))
|
36
|
+
print(_('save_api_key_prompt'))
|
37
37
|
if input() == 'y':
|
38
38
|
zshrc_path = os.path.expanduser('~/.zshrc')
|
39
39
|
with open(zshrc_path, 'a') as f:
|
40
40
|
f.write(f'export OPENAI_API_KEY={api_key}\n')
|
41
|
-
print(
|
42
|
-
f'APIキーを {zshrc_path} に保存しました。次回シェル起動時に読み込まれます。'
|
43
|
-
)
|
41
|
+
print(_('api_key_saved', zshrc_path))
|
44
42
|
os.environ['OPENAI_API_KEY'] = api_key
|
45
43
|
return api_key
|
46
44
|
else:
|
47
|
-
print('
|
45
|
+
print(_('api_key_required'))
|
48
46
|
return None
|
49
47
|
|
50
48
|
|
@@ -59,7 +57,7 @@ def validate_api_key(api_key: str) -> bool:
|
|
59
57
|
if response.status_code == 200:
|
60
58
|
return True
|
61
59
|
else:
|
62
|
-
print('
|
60
|
+
print(_('api_key_validation_error'))
|
63
61
|
return False
|
64
62
|
|
65
63
|
|
@@ -106,7 +104,7 @@ class ChatGPT:
|
|
106
104
|
try:
|
107
105
|
reply = responsej['choices'][0]['message']['content']
|
108
106
|
except KeyError:
|
109
|
-
print('
|
107
|
+
print(_('response_format_error') + str(responsej))
|
110
108
|
return 'Error: Unable to retrieve response.'
|
111
109
|
|
112
110
|
self.messages.append({'role': 'assistant', 'content': reply})
|
atcdr/util/i18n.py
ADDED
@@ -0,0 +1,317 @@
|
|
1
|
+
import locale
|
2
|
+
from typing import Dict, Optional
|
3
|
+
|
4
|
+
|
5
|
+
class I18n:
|
6
|
+
def __init__(self):
|
7
|
+
self._lang: Optional[str] = None
|
8
|
+
self._messages: Dict[str, Dict[str, str]] = {'ja': {}, 'en': {}}
|
9
|
+
self._load_messages()
|
10
|
+
self._detect_language()
|
11
|
+
|
12
|
+
def _detect_language(self) -> None:
|
13
|
+
try:
|
14
|
+
system_locale = locale.getdefaultlocale()[0]
|
15
|
+
if system_locale and system_locale.startswith('ja'):
|
16
|
+
self._lang = 'ja'
|
17
|
+
else:
|
18
|
+
self._lang = 'en'
|
19
|
+
except Exception:
|
20
|
+
self._lang = 'en'
|
21
|
+
|
22
|
+
def _load_messages(self) -> None:
|
23
|
+
self._messages = {
|
24
|
+
'ja': {
|
25
|
+
# download.py
|
26
|
+
'retry_problem': '再試行します。{}',
|
27
|
+
'redirect_occurred': 'リダイレクトが発生しました。{}',
|
28
|
+
'problem_not_found': '問題が見つかりません。{}',
|
29
|
+
'server_error': 'サーバーエラーが発生しました。{}',
|
30
|
+
'html_fetch_failed': '{}に対応するHTMLファイルを取得できませんでした。',
|
31
|
+
'save_failed': '{}の保存に失敗しました',
|
32
|
+
'file_saved': 'ファイルを保存しました: {}',
|
33
|
+
'solve_contest_problems': 'コンテストの問題を解きたい',
|
34
|
+
'download_one_problem': '1問だけダウンロードする',
|
35
|
+
'exit': '終了する',
|
36
|
+
'download_atcoder_html': 'AtCoderの問題のHTMLファイルをダウンロードします',
|
37
|
+
'navigate_with_arrows': '十字キーで移動,[enter]で実行',
|
38
|
+
'input_contest_name': 'コンテスト名を入力してください (例: abc012, abs, typical90)',
|
39
|
+
'which_problem_download': 'どの問題をダウンロードしますか?',
|
40
|
+
'exiting': '終了します',
|
41
|
+
'invalid_selection': '無効な選択です',
|
42
|
+
'specify_contest_name': 'コンテスト名を指定してください',
|
43
|
+
'invalid_download_args': 'ダウンロードの引数が正しくありません',
|
44
|
+
# test.py
|
45
|
+
'runner_not_found': '{}の適切な言語のランナーが見つかりませんでした.',
|
46
|
+
'test_of': '{}のテスト \n',
|
47
|
+
'compile_time': 'コンパイルにかかった時間: [not italic cyan]{}[/] ms[/]',
|
48
|
+
'compiler_message': 'コンパイラーのメッセージ',
|
49
|
+
'status': 'ステータス ',
|
50
|
+
'execution_time': '実行時間 [cyan]{}[/cyan] ms',
|
51
|
+
'input': '入力',
|
52
|
+
'output': '出力',
|
53
|
+
'expected_output': '正解の出力',
|
54
|
+
'test_in_progress': 'テスト進行中',
|
55
|
+
'test_completed': 'テスト完了',
|
56
|
+
'problem_file_not_found': '問題のファイルが見つかりません。\n問題のファイルが存在するディレクトリーに移動してから実行してください。',
|
57
|
+
# markdown.py
|
58
|
+
'markdown_created': 'Markdownファイルを作成しました.',
|
59
|
+
# login.py
|
60
|
+
'already_logged_in': 'すでにログインしています. ',
|
61
|
+
'username': 'ユーザー名: ',
|
62
|
+
'password': 'パスワード: ',
|
63
|
+
'solve_captcha': 'キャプチャー認証を解決してください',
|
64
|
+
'logging_in': 'ログインします',
|
65
|
+
'waiting_login_result': 'ログインの結果の待機中...',
|
66
|
+
'login_success': 'ログイン成功!',
|
67
|
+
'error': 'エラー: {}',
|
68
|
+
# submit.py
|
69
|
+
'select_implementation': '以下の一覧から{}の実装/コンパイラーを選択してください',
|
70
|
+
'submission_failed': '提出に失敗しました',
|
71
|
+
'submission_id_not_found': '提出IDが取得できませんでした',
|
72
|
+
'submission_success': '提出に成功しました!',
|
73
|
+
'waiting_judge': 'ジャッジ待機中',
|
74
|
+
'judge_timeout': '15秒待ってもジャッジが開始されませんでした',
|
75
|
+
'judging': 'ジャッジ中',
|
76
|
+
'judge_completed': 'ジャッジ完了',
|
77
|
+
'not_logged_in': 'ログインしていません.',
|
78
|
+
'login_failed': 'ログインに失敗しました.',
|
79
|
+
'sample_not_ac': 'サンプルケースが AC していないので提出できません',
|
80
|
+
'logout_success': 'ログアウトしました.',
|
81
|
+
'submission_details': '提出ID: {}, URL: {}',
|
82
|
+
# open.py
|
83
|
+
'not_found': 'が見つかりません',
|
84
|
+
'url_opened': 'URLを開きました',
|
85
|
+
'url_not_found_in': 'にURLが見つかりませんでした',
|
86
|
+
# generate.py
|
87
|
+
'generating_code': 'コード生成中 (by {})',
|
88
|
+
'code_generation_success': 'コードの生成に成功しました. ',
|
89
|
+
'code_by_model': '{}による{}コード',
|
90
|
+
'code_saved': '{} の出力したコードを保存しました:{}',
|
91
|
+
'generating_template': '{}のテンプレートを生成しています...',
|
92
|
+
'template_created': 'テンプレートファイルを作成 :{}',
|
93
|
+
'nth_code_generation': '{}回目のコード生成中 (by {})',
|
94
|
+
'regenerating_with_prompt': '次のプロンプトを{}に与え,再生成します',
|
95
|
+
'code_generation_success_file': 'コードの生成に成功しました!:{}',
|
96
|
+
'testing_generated_code': '{}が生成したコードをテスト中',
|
97
|
+
'test_success': 'コードのテストに成功!',
|
98
|
+
'test_failed': 'コードのテストに失敗!',
|
99
|
+
'log_saved': '{}の出力のログを保存しました:{}',
|
100
|
+
# util/gpt.py
|
101
|
+
'api_key_validation_failed': '環境変数に設定されているAPIキーの検証に失敗しました ',
|
102
|
+
'get_api_key_prompt': 'https://platform.openai.com/api-keys からchatGPTのAPIキーを入手しましょう。\nAPIキー入力してください: ',
|
103
|
+
'api_key_test_success': 'APIキーのテストに成功しました。',
|
104
|
+
'save_api_key_prompt': '以下, ~/.zshrcにAPIキーを保存しますか? [y/n]',
|
105
|
+
'api_key_saved': 'APIキーを {} に保存しました。次回シェル起動時に読み込まれます。',
|
106
|
+
'api_key_required': 'コード生成にはAPIキーが必要です。',
|
107
|
+
'api_key_validation_error': 'APIキーの検証に失敗しました。',
|
108
|
+
'response_format_error': 'Error:レスポンスの形式が正しくありません. \n',
|
109
|
+
# util/fileops.py
|
110
|
+
'multiple_files_found': '複数のファイルが見つかりました.ファイルを選択してください:',
|
111
|
+
'target_file_not_found': '対象ファイルが見つかりません。',
|
112
|
+
'file_not_selected': 'ファイルが選択されませんでした。',
|
113
|
+
# util/problem.py and util/parse.py
|
114
|
+
'name_required': 'nameは必須です',
|
115
|
+
'language_not_supported': '言語は {} に対応していません',
|
116
|
+
'form_not_found': '問題ページにフォームが存在しません',
|
117
|
+
'problem_table_not_found': '問題のテーブルが見つかりませんでした.',
|
118
|
+
'tbody_not_found': 'tbodyが見つかりませんでした.',
|
119
|
+
# util/session.py
|
120
|
+
'response_info': 'レスポンス情報',
|
121
|
+
'item': '項目',
|
122
|
+
'content': '内容',
|
123
|
+
'status_code': 'ステータスコード',
|
124
|
+
'reason': '理由',
|
125
|
+
'response_headers': 'レスポンスヘッダー',
|
126
|
+
'key': 'キー',
|
127
|
+
'value': '値',
|
128
|
+
'redirect_history': 'リダイレクト履歴',
|
129
|
+
'step': 'ステップ',
|
130
|
+
'response_body': 'レスポンスボディ',
|
131
|
+
'hello_user': 'こんにちは![cyan]{}[/] さん',
|
132
|
+
'session_check_error': 'セッションチェック中にエラーが発生しました: {}',
|
133
|
+
# Click command descriptions
|
134
|
+
'cmd_download': 'AtCoder の問題をダウンロード',
|
135
|
+
'cmd_test': 'テストを実行',
|
136
|
+
'cmd_submit': 'ソースを提出',
|
137
|
+
'cmd_generate': 'コードを生成',
|
138
|
+
'cmd_login': 'AtCoderへログイン',
|
139
|
+
'cmd_logout': 'AtCoderへログアウト',
|
140
|
+
'cmd_markdown': 'Markdown形式で問題を表示します',
|
141
|
+
'cmd_open': 'HTMLファイルを開く',
|
142
|
+
# Click option descriptions
|
143
|
+
'opt_no_test': 'テストをスキップ',
|
144
|
+
'opt_no_feedback': 'フィードバックをスキップ',
|
145
|
+
'opt_lang': '出力する言語を指定',
|
146
|
+
'opt_save': '変換結果をファイルに保存',
|
147
|
+
'opt_output_lang': '出力するプログラミング言語',
|
148
|
+
'opt_model': '使用するGPTモデル',
|
149
|
+
'opt_without_test': 'テストケースを省略して生成',
|
150
|
+
'opt_template': 'テンプレートを生成',
|
151
|
+
},
|
152
|
+
'en': {
|
153
|
+
# download.py
|
154
|
+
'retry_problem': 'Retrying... {}',
|
155
|
+
'redirect_occurred': 'Redirect occurred. {}',
|
156
|
+
'problem_not_found': 'Problem not found. {}',
|
157
|
+
'server_error': 'Server error occurred. {}',
|
158
|
+
'html_fetch_failed': 'Failed to fetch HTML file for {}.',
|
159
|
+
'save_failed': 'Failed to save {}',
|
160
|
+
'file_saved': 'File saved: {}',
|
161
|
+
'solve_contest_problems': 'Solve contest problems',
|
162
|
+
'download_one_problem': 'Download one problem',
|
163
|
+
'exit': 'Exit',
|
164
|
+
'download_atcoder_html': 'Download AtCoder problem HTML files',
|
165
|
+
'navigate_with_arrows': 'Use arrow keys to navigate, [enter] to execute',
|
166
|
+
'input_contest_name': 'Enter contest name (e.g., abc012, abs, typical90)',
|
167
|
+
'which_problem_download': 'Which problem to download?',
|
168
|
+
'exiting': 'Exiting',
|
169
|
+
'invalid_selection': 'Invalid selection',
|
170
|
+
'specify_contest_name': 'Please specify contest name',
|
171
|
+
'invalid_download_args': 'Invalid download arguments',
|
172
|
+
# test.py
|
173
|
+
'runner_not_found': 'Appropriate language runner for {} not found.',
|
174
|
+
'test_of': 'Testing {} \n',
|
175
|
+
'compile_time': 'Compile time: [not italic cyan]{}[/] ms[/]',
|
176
|
+
'compiler_message': 'Compiler message',
|
177
|
+
'status': 'Status ',
|
178
|
+
'execution_time': 'Execution time [cyan]{}[/cyan] ms',
|
179
|
+
'input': 'Input',
|
180
|
+
'output': 'Output',
|
181
|
+
'expected_output': 'Expected output',
|
182
|
+
'test_in_progress': 'Testing in progress',
|
183
|
+
'test_completed': 'Test completed',
|
184
|
+
'problem_file_not_found': 'Problem file not found.\nPlease navigate to the directory containing the problem file.',
|
185
|
+
# markdown.py
|
186
|
+
'markdown_created': 'Markdown file created.',
|
187
|
+
# login.py
|
188
|
+
'already_logged_in': 'Already logged in. ',
|
189
|
+
'username': 'Username: ',
|
190
|
+
'password': 'Password: ',
|
191
|
+
'solve_captcha': 'Please solve the captcha',
|
192
|
+
'logging_in': 'Logging in',
|
193
|
+
'waiting_login_result': 'Waiting for login result...',
|
194
|
+
'login_success': 'Login successful!',
|
195
|
+
'error': 'Error: {}',
|
196
|
+
# submit.py
|
197
|
+
'select_implementation': 'Select {} implementation/compiler from the list below',
|
198
|
+
'submission_failed': 'Submission failed',
|
199
|
+
'submission_id_not_found': 'Submission ID not found',
|
200
|
+
'submission_success': 'Submission successful!',
|
201
|
+
'waiting_judge': 'Waiting for judge',
|
202
|
+
'judge_timeout': 'Judge did not start after 15 seconds',
|
203
|
+
'judging': 'Judging',
|
204
|
+
'judge_completed': 'Judge completed',
|
205
|
+
'not_logged_in': 'Not logged in.',
|
206
|
+
'login_failed': 'Login failed.',
|
207
|
+
'sample_not_ac': 'Cannot submit because sample cases are not AC',
|
208
|
+
'logout_success': 'Logged out successfully.',
|
209
|
+
'submission_details': 'Submission ID: {}, URL: {}',
|
210
|
+
# open.py
|
211
|
+
'not_found': ' not found',
|
212
|
+
'url_opened': 'URL opened',
|
213
|
+
'url_not_found_in': 'URL not found in ',
|
214
|
+
# generate.py
|
215
|
+
'generating_code': 'Generating code (by {})',
|
216
|
+
'code_generation_success': 'Code generation successful. ',
|
217
|
+
'code_by_model': '{} code by {}',
|
218
|
+
'code_saved': 'Code generated by {} saved: {}',
|
219
|
+
'generating_template': 'Generating {} template...',
|
220
|
+
'template_created': 'Template file created: {}',
|
221
|
+
'nth_code_generation': 'Code generation attempt {} (by {})',
|
222
|
+
'regenerating_with_prompt': 'Regenerating with the following prompt for {}',
|
223
|
+
'code_generation_success_file': 'Code generation successful: {}',
|
224
|
+
'testing_generated_code': 'Testing code generated by {}',
|
225
|
+
'test_success': 'Code test successful!',
|
226
|
+
'test_failed': 'Code test failed!',
|
227
|
+
'log_saved': 'Log of {} output saved: {}',
|
228
|
+
# util/gpt.py
|
229
|
+
'api_key_validation_failed': 'API key validation failed ',
|
230
|
+
'get_api_key_prompt': 'Get your ChatGPT API key from https://platform.openai.com/api-keys\nEnter API key: ',
|
231
|
+
'api_key_test_success': 'API key test successful.',
|
232
|
+
'save_api_key_prompt': 'Save API key to ~/.zshrc? [y/n]',
|
233
|
+
'api_key_saved': 'API key saved to {}. Will be loaded on next shell startup.',
|
234
|
+
'api_key_required': 'API key required for code generation.',
|
235
|
+
'api_key_validation_error': 'API key validation failed.',
|
236
|
+
'response_format_error': 'Error: Response format is invalid. \n',
|
237
|
+
# util/fileops.py
|
238
|
+
'multiple_files_found': 'Multiple files found. Please select a file:',
|
239
|
+
'target_file_not_found': 'Target file not found.',
|
240
|
+
'file_not_selected': 'No file selected.',
|
241
|
+
# util/problem.py and util/parse.py
|
242
|
+
'name_required': 'Name is required',
|
243
|
+
'language_not_supported': 'Language {} is not supported',
|
244
|
+
'form_not_found': 'Form not found on problem page',
|
245
|
+
'problem_table_not_found': 'Problem table not found.',
|
246
|
+
'tbody_not_found': 'tbody not found.',
|
247
|
+
# util/session.py
|
248
|
+
'response_info': 'Response Information',
|
249
|
+
'item': 'Item',
|
250
|
+
'content': 'Content',
|
251
|
+
'status_code': 'Status Code',
|
252
|
+
'reason': 'Reason',
|
253
|
+
'response_headers': 'Response Headers',
|
254
|
+
'key': 'Key',
|
255
|
+
'value': 'Value',
|
256
|
+
'redirect_history': 'Redirect History',
|
257
|
+
'step': 'Step',
|
258
|
+
'response_body': 'Response Body',
|
259
|
+
'hello_user': 'Hello [cyan]{}[/]!',
|
260
|
+
'session_check_error': 'Error occurred during session check: {}',
|
261
|
+
# Click command descriptions
|
262
|
+
'cmd_download': 'Download AtCoder problems',
|
263
|
+
'cmd_test': 'Run tests',
|
264
|
+
'cmd_submit': 'Submit source code',
|
265
|
+
'cmd_generate': 'Generate code',
|
266
|
+
'cmd_login': 'Login to AtCoder',
|
267
|
+
'cmd_logout': 'Logout from AtCoder',
|
268
|
+
'cmd_markdown': 'Display problem in Markdown format',
|
269
|
+
'cmd_open': 'Open HTML file',
|
270
|
+
# Click option descriptions
|
271
|
+
'opt_no_test': 'Skip testing',
|
272
|
+
'opt_no_feedback': 'Skip feedback',
|
273
|
+
'opt_lang': 'Specify output language',
|
274
|
+
'opt_save': 'Save conversion result to file',
|
275
|
+
'opt_output_lang': 'Specify output programming language',
|
276
|
+
'opt_model': 'GPT model to use',
|
277
|
+
'opt_without_test': 'Generate without test cases',
|
278
|
+
'opt_template': 'Generate template',
|
279
|
+
},
|
280
|
+
}
|
281
|
+
|
282
|
+
def get(self, key: str, *args) -> str:
|
283
|
+
"""
|
284
|
+
メッセージを取得する
|
285
|
+
Args:
|
286
|
+
key: メッセージキー
|
287
|
+
*args: フォーマット引数
|
288
|
+
Returns:
|
289
|
+
フォーマット済みのメッセージ
|
290
|
+
"""
|
291
|
+
message = self._messages.get(self._lang or 'en', {}).get(key, key)
|
292
|
+
if args:
|
293
|
+
try:
|
294
|
+
return message.format(*args)
|
295
|
+
except Exception:
|
296
|
+
return message
|
297
|
+
return message
|
298
|
+
|
299
|
+
def set_language(self, lang: str) -> None:
|
300
|
+
"""言語を明示的に設定する"""
|
301
|
+
if lang in self._messages:
|
302
|
+
self._lang = lang
|
303
|
+
|
304
|
+
@property
|
305
|
+
def language(self) -> str:
|
306
|
+
"""現在の言語を取得する"""
|
307
|
+
return self._lang or 'en'
|
308
|
+
|
309
|
+
|
310
|
+
# シングルトンインスタンス
|
311
|
+
i18n = I18n()
|
312
|
+
|
313
|
+
|
314
|
+
# 便利な関数
|
315
|
+
def _(key: str, *args) -> str:
|
316
|
+
"""i18n.get()のショートカット"""
|
317
|
+
return i18n.get(key, *args)
|
atcdr/util/parse.py
CHANGED
@@ -5,6 +5,8 @@ from bs4 import BeautifulSoup as bs
|
|
5
5
|
from bs4 import Tag
|
6
6
|
from markdownify import MarkdownConverter
|
7
7
|
|
8
|
+
from atcdr.util.i18n import _, i18n
|
9
|
+
|
8
10
|
|
9
11
|
class HTML:
|
10
12
|
def __init__(self, html: str) -> None:
|
@@ -68,13 +70,16 @@ class ProblemForm(Tag):
|
|
68
70
|
|
69
71
|
|
70
72
|
class ProblemHTML(HTML):
|
71
|
-
def repair_me(self) -> None:
|
73
|
+
def repair_me(self, lang: Optional[str] = None) -> None:
|
72
74
|
html = self.html.replace('//img.atcoder.jp', 'https://img.atcoder.jp')
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
75
|
+
# 言語パラメータまたはi18nの設定に基づいて言語を決定
|
76
|
+
target_lang = lang or i18n.language
|
77
|
+
if target_lang == 'ja':
|
78
|
+
html = html.replace(
|
79
|
+
'<meta http-equiv="Content-Language" content="en">',
|
80
|
+
'<meta http-equiv="Content-Language" content="ja">',
|
81
|
+
)
|
82
|
+
html = html.replace('LANG = "en"', 'LANG="ja"')
|
78
83
|
self.soup = bs(html, 'html.parser')
|
79
84
|
|
80
85
|
def abstract_problem_part(self, lang: str) -> Optional[Tag]:
|
@@ -87,7 +92,7 @@ class ProblemHTML(HTML):
|
|
87
92
|
elif lang == 'en':
|
88
93
|
lang_class = 'lang-en'
|
89
94
|
else:
|
90
|
-
raise ValueError(
|
95
|
+
raise ValueError(_('language_not_supported', lang))
|
91
96
|
span = task_statement.find('span', {'class': lang_class})
|
92
97
|
return span
|
93
98
|
|
@@ -105,7 +110,7 @@ class ProblemHTML(HTML):
|
|
105
110
|
def load_labeled_testcase(self) -> List:
|
106
111
|
from atcdr.test import LabeledTestCase, TestCase
|
107
112
|
|
108
|
-
problem_part = self.abstract_problem_part(
|
113
|
+
problem_part = self.abstract_problem_part(i18n.language)
|
109
114
|
if problem_part is None:
|
110
115
|
return []
|
111
116
|
|
@@ -146,7 +151,7 @@ class ProblemHTML(HTML):
|
|
146
151
|
def form(self) -> ProblemForm:
|
147
152
|
form = self.soup.find('form', class_='form-code-submit')
|
148
153
|
if not isinstance(form, Tag):
|
149
|
-
raise ValueError('
|
154
|
+
raise ValueError(_('form_not_found'))
|
150
155
|
form.__class__ = ProblemForm
|
151
156
|
return form
|
152
157
|
|
@@ -178,12 +183,12 @@ def get_problem_urls_from_tasks(html_content: str) -> list[tuple[str, str]]:
|
|
178
183
|
soup = bs(html_content, 'html.parser')
|
179
184
|
table = soup.find('table')
|
180
185
|
if not table:
|
181
|
-
raise ValueError('
|
186
|
+
raise ValueError(_('problem_table_not_found'))
|
182
187
|
|
183
188
|
# tbodyタグを見つける
|
184
189
|
tbody = table.find('tbody')
|
185
190
|
if not tbody:
|
186
|
-
raise ValueError('
|
191
|
+
raise ValueError(_('tbody_not_found'))
|
187
192
|
|
188
193
|
# tbody内の1列目のaタグのリンクと中身を取得
|
189
194
|
links = []
|
atcdr/util/problem.py
CHANGED
@@ -3,19 +3,20 @@ from dataclasses import dataclass
|
|
3
3
|
|
4
4
|
import requests
|
5
5
|
|
6
|
+
from atcdr.util.i18n import _
|
6
7
|
from atcdr.util.parse import get_problem_urls_from_tasks
|
7
8
|
|
8
9
|
|
9
10
|
class Contest:
|
10
11
|
def __init__(self, name: str, session: requests.Session):
|
11
12
|
if not name:
|
12
|
-
raise ValueError('
|
13
|
+
raise ValueError(_('name_required'))
|
13
14
|
self.name = name
|
14
15
|
|
15
16
|
self.url = f'https://atcoder.jp/contests/{name}/tasks'
|
16
17
|
retry_attempts = 2
|
17
18
|
retry_wait = 0.30
|
18
|
-
for
|
19
|
+
for attempt in range(retry_attempts):
|
19
20
|
response = session.get(self.url)
|
20
21
|
if response.ok:
|
21
22
|
break
|