AtCoderStudyBooster 0.3.3__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 +139 -234
- 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 +29 -75
- 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.3.3.dist-info/METADATA +0 -213
- atcoderstudybooster-0.3.3.dist-info/RECORD +0 -21
- {atcoderstudybooster-0.3.3.dist-info → atcoderstudybooster-0.4.1.dist-info}/WHEEL +0 -0
- {atcoderstudybooster-0.3.3.dist-info → atcoderstudybooster-0.4.1.dist-info}/entry_points.txt +0 -0
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
@@ -1,94 +1,48 @@
|
|
1
|
-
|
1
|
+
import time
|
2
|
+
from dataclasses import dataclass
|
2
3
|
|
3
4
|
import requests
|
4
5
|
|
6
|
+
from atcdr.util.i18n import _
|
5
7
|
from atcdr.util.parse import get_problem_urls_from_tasks
|
6
8
|
|
7
9
|
|
8
10
|
class Contest:
|
9
|
-
def __init__(self, name: str,
|
11
|
+
def __init__(self, name: str, session: requests.Session):
|
10
12
|
if not name:
|
11
|
-
raise ValueError('
|
12
|
-
self.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@property
|
28
|
-
def url(self) -> str:
|
29
|
-
return f'https://atcoder.jp/contests/{self.contest}/tasks'
|
30
|
-
|
31
|
-
def __str__(self) -> str:
|
32
|
-
return f'{self.contest}'
|
33
|
-
|
34
|
-
def __repr__(self) -> str:
|
35
|
-
return f'Contest(name={self._name}, number={self._number})'
|
36
|
-
|
37
|
-
def problems(self, session: Optional[requests.Session] = None) -> List['Problem']:
|
38
|
-
session = session or requests.Session()
|
39
|
-
response = session.get(self.url)
|
40
|
-
|
41
|
-
if response.status_code != 200:
|
42
|
-
return []
|
43
|
-
|
44
|
-
return [
|
45
|
-
Problem(self, label=label, url=url)
|
13
|
+
raise ValueError(_('name_required'))
|
14
|
+
self.name = name
|
15
|
+
|
16
|
+
self.url = f'https://atcoder.jp/contests/{name}/tasks'
|
17
|
+
retry_attempts = 2
|
18
|
+
retry_wait = 0.30
|
19
|
+
for attempt in range(retry_attempts):
|
20
|
+
response = session.get(self.url)
|
21
|
+
if response.ok:
|
22
|
+
break
|
23
|
+
else:
|
24
|
+
time.sleep(retry_wait)
|
25
|
+
|
26
|
+
self.problems = [
|
27
|
+
Problem(url=url, contest=self, label=label)
|
46
28
|
for label, url in get_problem_urls_from_tasks(response.text)
|
47
29
|
]
|
48
30
|
|
49
|
-
|
50
|
-
|
51
|
-
def __new__(cls, diff: str) -> 'Diff':
|
52
|
-
if isinstance(diff, str) and len(diff) == 1 and diff.isalpha():
|
53
|
-
return super().__new__(cls, diff.upper())
|
54
|
-
raise ValueError('diffは英大文字または小文字の1文字である必要があります')
|
31
|
+
def __str__(self) -> str:
|
32
|
+
return f'{self.name}'
|
55
33
|
|
56
34
|
def __repr__(self) -> str:
|
57
|
-
return f
|
35
|
+
return f'Contest(name={self.name})'
|
58
36
|
|
59
37
|
|
38
|
+
@dataclass
|
60
39
|
class Problem:
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
difficulty: Optional[Diff] = None,
|
65
|
-
label: Optional[str] = None,
|
66
|
-
url: Optional[str] = None,
|
67
|
-
):
|
68
|
-
self._contest = contest
|
69
|
-
if difficulty:
|
70
|
-
self._label = difficulty.upper()
|
71
|
-
self._url = contest.url + f'/{contest}_{difficulty.lower()}'
|
72
|
-
elif label and url:
|
73
|
-
self._label = label
|
74
|
-
self._url = url
|
75
|
-
else:
|
76
|
-
raise ValueError('labelとurlは両方必須かdifficultyが必要です')
|
77
|
-
|
78
|
-
@property
|
79
|
-
def contest(self) -> Contest:
|
80
|
-
return self._contest
|
40
|
+
url: str
|
41
|
+
contest: Contest
|
42
|
+
label: str
|
81
43
|
|
82
|
-
|
83
|
-
|
84
|
-
return self._label
|
85
|
-
|
86
|
-
@property
|
87
|
-
def url(self) -> str:
|
88
|
-
return self._url
|
44
|
+
def __str__(self) -> str:
|
45
|
+
return f'{self.label} - {self.contest.name}'
|
89
46
|
|
90
47
|
def __repr__(self) -> str:
|
91
|
-
return f'Problem(
|
92
|
-
|
93
|
-
def __str__(self) -> str:
|
94
|
-
return f'{self.contest} {self.label}'
|
48
|
+
return f'Problem(url={self.url}, contest={self.contest}, label={self.label})'
|
atcdr/util/session.py
CHANGED
@@ -9,6 +9,7 @@ from rich.panel import Panel
|
|
9
9
|
from rich.syntax import Syntax
|
10
10
|
from rich.table import Table
|
11
11
|
|
12
|
+
from atcdr.util.i18n import _
|
12
13
|
from atcdr.util.parse import get_username_from_html
|
13
14
|
|
14
15
|
COOKIE_PATH = os.path.join(os.path.expanduser('~'), '.cache', 'atcdr', 'session.json')
|
@@ -19,18 +20,18 @@ def print_rich_response(
|
|
19
20
|
response: requests.Response, body_range: tuple = (0, 24)
|
20
21
|
) -> None:
|
21
22
|
# レスポンス情報をテーブル形式で表示
|
22
|
-
info_table = Table(title='
|
23
|
-
info_table.add_column('
|
24
|
-
info_table.add_column('
|
23
|
+
info_table = Table(title=_('response_info'))
|
24
|
+
info_table.add_column(_('item'), justify='left', style='cyan', no_wrap=True)
|
25
|
+
info_table.add_column(_('content'), justify='left', style='magenta')
|
25
26
|
info_table.add_row('URL', response.url)
|
26
|
-
info_table.add_row('
|
27
|
-
info_table.add_row('
|
27
|
+
info_table.add_row(_('status_code'), str(response.status_code))
|
28
|
+
info_table.add_row(_('reason'), response.reason)
|
28
29
|
info_table = Align.center(info_table)
|
29
30
|
|
30
31
|
# ヘッダー情報をテーブル形式で表示
|
31
|
-
header_table = Table(title='
|
32
|
-
header_table.add_column('
|
33
|
-
header_table.add_column('
|
32
|
+
header_table = Table(title=_('response_headers'))
|
33
|
+
header_table.add_column(_('key'), style='cyan', no_wrap=True)
|
34
|
+
header_table.add_column(_('value'), style='magenta', overflow='fold')
|
34
35
|
for key, value in response.headers.items():
|
35
36
|
value = unquote(value)
|
36
37
|
header_table.add_row(key, value)
|
@@ -39,9 +40,9 @@ def print_rich_response(
|
|
39
40
|
# リダイレクトの歴史
|
40
41
|
redirect_table = None
|
41
42
|
if response.history:
|
42
|
-
redirect_table = Table(title='
|
43
|
-
redirect_table.add_column('
|
44
|
-
redirect_table.add_column('
|
43
|
+
redirect_table = Table(title=_('redirect_history'))
|
44
|
+
redirect_table.add_column(_('step'), style='cyan')
|
45
|
+
redirect_table.add_column(_('status_code'), style='magenta')
|
45
46
|
redirect_table.add_column('URL', style='green')
|
46
47
|
for i, redirect_response in enumerate(response.history):
|
47
48
|
redirect_table.add_row(
|
@@ -80,7 +81,7 @@ def print_rich_response(
|
|
80
81
|
if response.text
|
81
82
|
else None
|
82
83
|
)
|
83
|
-
body_panel = Panel(body, title='
|
84
|
+
body_panel = Panel(body, title=_('response_body')) if body else None
|
84
85
|
|
85
86
|
print(info_table)
|
86
87
|
print(header_table)
|
@@ -102,7 +103,7 @@ def load_session() -> requests.Session:
|
|
102
103
|
response = session.get(ATCODER_URL)
|
103
104
|
username = get_username_from_html(response.text)
|
104
105
|
if username:
|
105
|
-
print(
|
106
|
+
print(_('hello_user', username))
|
106
107
|
return session
|
107
108
|
else:
|
108
109
|
return requests.Session()
|
@@ -131,7 +132,7 @@ def validate_session(session: requests.Session) -> bool:
|
|
131
132
|
return False
|
132
133
|
return False
|
133
134
|
except requests.RequestException as e:
|
134
|
-
print(
|
135
|
+
print('[red][-][/] ' + _('session_check_error', e))
|
135
136
|
return False
|
136
137
|
|
137
138
|
|