thonny-codemate 0.1.0__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.
- thonny_codemate-0.1.0.dist-info/METADATA +307 -0
- thonny_codemate-0.1.0.dist-info/RECORD +27 -0
- thonny_codemate-0.1.0.dist-info/WHEEL +5 -0
- thonny_codemate-0.1.0.dist-info/licenses/LICENSE +21 -0
- thonny_codemate-0.1.0.dist-info/top_level.txt +1 -0
- thonnycontrib/__init__.py +1 -0
- thonnycontrib/thonny_codemate/__init__.py +397 -0
- thonnycontrib/thonny_codemate/api.py +154 -0
- thonnycontrib/thonny_codemate/context_manager.py +296 -0
- thonnycontrib/thonny_codemate/external_providers.py +714 -0
- thonnycontrib/thonny_codemate/i18n.py +506 -0
- thonnycontrib/thonny_codemate/llm_client.py +841 -0
- thonnycontrib/thonny_codemate/message_virtualization.py +136 -0
- thonnycontrib/thonny_codemate/model_manager.py +515 -0
- thonnycontrib/thonny_codemate/performance_monitor.py +141 -0
- thonnycontrib/thonny_codemate/prompts.py +102 -0
- thonnycontrib/thonny_codemate/ui/__init__.py +1 -0
- thonnycontrib/thonny_codemate/ui/chat_view.py +687 -0
- thonnycontrib/thonny_codemate/ui/chat_view_html.py +1299 -0
- thonnycontrib/thonny_codemate/ui/custom_prompt_dialog.py +175 -0
- thonnycontrib/thonny_codemate/ui/markdown_renderer.py +484 -0
- thonnycontrib/thonny_codemate/ui/model_download_dialog.py +355 -0
- thonnycontrib/thonny_codemate/ui/settings_dialog.py +1218 -0
- thonnycontrib/thonny_codemate/utils/__init__.py +25 -0
- thonnycontrib/thonny_codemate/utils/constants.py +138 -0
- thonnycontrib/thonny_codemate/utils/error_messages.py +92 -0
- thonnycontrib/thonny_codemate/utils/unified_error_handler.py +310 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
"""
|
2
|
+
ユーティリティモジュール
|
3
|
+
"""
|
4
|
+
# 統一エラーハンドラーからすべてをインポート
|
5
|
+
from .unified_error_handler import (
|
6
|
+
ErrorContext,
|
7
|
+
log_error_with_context,
|
8
|
+
handle_api_error,
|
9
|
+
with_error_handling,
|
10
|
+
error_context,
|
11
|
+
retry_operation,
|
12
|
+
retry_decorator,
|
13
|
+
safe_execute
|
14
|
+
)
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
'ErrorContext',
|
18
|
+
'log_error_with_context',
|
19
|
+
'handle_api_error',
|
20
|
+
'with_error_handling',
|
21
|
+
'error_context',
|
22
|
+
'retry_operation',
|
23
|
+
'retry_decorator',
|
24
|
+
'safe_execute',
|
25
|
+
]
|
@@ -0,0 +1,138 @@
|
|
1
|
+
"""
|
2
|
+
定数定義モジュール
|
3
|
+
マジックナンバーやハードコードされた値を集約
|
4
|
+
"""
|
5
|
+
|
6
|
+
# UI関連の定数
|
7
|
+
class UIConstants:
|
8
|
+
# ウィンドウサイズ
|
9
|
+
SETTINGS_WINDOW_SIZE = (700, 750)
|
10
|
+
|
11
|
+
# ボタン幅
|
12
|
+
BUTTON_WIDTH_LARGE = 20
|
13
|
+
BUTTON_WIDTH_SMALL = 12
|
14
|
+
BUTTON_WIDTH_MEDIUM = 15
|
15
|
+
|
16
|
+
# タイミング
|
17
|
+
QUEUE_CHECK_INTERVAL_MS = 50
|
18
|
+
HTML_READY_TIMEOUT_MS = 200
|
19
|
+
NOTIFICATION_DURATION_MS = 2000
|
20
|
+
|
21
|
+
# メッセージ制限
|
22
|
+
MAX_MESSAGES_IN_MEMORY = 200
|
23
|
+
|
24
|
+
|
25
|
+
# LLM関連の定数
|
26
|
+
class LLMConstants:
|
27
|
+
# モデルのデフォルト値
|
28
|
+
DEFAULT_CONTEXT_SIZE = 4096
|
29
|
+
DEFAULT_MAX_TOKENS = 2048
|
30
|
+
DEFAULT_TEMPERATURE = 0.3
|
31
|
+
DEFAULT_REPEAT_PENALTY = 1.1
|
32
|
+
DEFAULT_TOP_P = 0.95
|
33
|
+
DEFAULT_TOP_K = 40
|
34
|
+
|
35
|
+
# メモリ制限
|
36
|
+
MAX_CONVERSATION_HISTORY = 10
|
37
|
+
|
38
|
+
# ストップトークン
|
39
|
+
STOP_TOKENS = ["</s>", "\n\n\n"]
|
40
|
+
CHAT_STOP_TOKENS = ["</s>"]
|
41
|
+
|
42
|
+
|
43
|
+
# プロバイダー固有の設定
|
44
|
+
class ProviderConstants:
|
45
|
+
# APIキーオプション名のマッピング
|
46
|
+
API_KEY_OPTIONS = {
|
47
|
+
"chatgpt": "llm.chatgpt_api_key",
|
48
|
+
"openrouter": "llm.openrouter_api_key"
|
49
|
+
}
|
50
|
+
|
51
|
+
# プロバイダー別モデルリスト
|
52
|
+
PROVIDER_MODELS = {
|
53
|
+
"chatgpt": [
|
54
|
+
"gpt-4o",
|
55
|
+
"gpt-4o-mini",
|
56
|
+
"gpt-4-turbo",
|
57
|
+
"gpt-4",
|
58
|
+
"o1-preview",
|
59
|
+
"o1-mini"
|
60
|
+
],
|
61
|
+
"openrouter": [
|
62
|
+
"meta-llama/llama-3.2-3b-instruct:free",
|
63
|
+
"meta-llama/llama-3.2-1b-instruct:free",
|
64
|
+
"google/gemini-2.0-flash-exp:free",
|
65
|
+
"anthropic/claude-3.5-sonnet",
|
66
|
+
"openai/gpt-4o"
|
67
|
+
]
|
68
|
+
}
|
69
|
+
|
70
|
+
# デフォルトポート
|
71
|
+
DEFAULT_PORTS = {
|
72
|
+
"ollama": 11434,
|
73
|
+
"lmstudio": 1234
|
74
|
+
}
|
75
|
+
|
76
|
+
# デフォルトベースURL
|
77
|
+
DEFAULT_BASE_URLS = {
|
78
|
+
"ollama": "http://localhost:11434",
|
79
|
+
"lmstudio": "http://localhost:1234"
|
80
|
+
}
|
81
|
+
|
82
|
+
|
83
|
+
# ファイル拡張子と言語のマッピング
|
84
|
+
LANGUAGE_EXTENSIONS = {
|
85
|
+
'.py': 'Python',
|
86
|
+
'.pyw': 'Python',
|
87
|
+
'.js': 'JavaScript',
|
88
|
+
'.mjs': 'JavaScript',
|
89
|
+
'.cjs': 'JavaScript',
|
90
|
+
'.ts': 'TypeScript',
|
91
|
+
'.tsx': 'TypeScript',
|
92
|
+
'.jsx': 'JavaScript',
|
93
|
+
'.java': 'Java',
|
94
|
+
'.cpp': 'C++',
|
95
|
+
'.cxx': 'C++',
|
96
|
+
'.cc': 'C++',
|
97
|
+
'.c': 'C',
|
98
|
+
'.h': 'C/C++',
|
99
|
+
'.hpp': 'C++',
|
100
|
+
'.cs': 'C#',
|
101
|
+
'.go': 'Go',
|
102
|
+
'.rs': 'Rust',
|
103
|
+
'.php': 'PHP',
|
104
|
+
'.rb': 'Ruby',
|
105
|
+
'.swift': 'Swift',
|
106
|
+
'.kt': 'Kotlin',
|
107
|
+
'.scala': 'Scala',
|
108
|
+
'.r': 'R',
|
109
|
+
'.R': 'R',
|
110
|
+
'.lua': 'Lua',
|
111
|
+
'.pl': 'Perl',
|
112
|
+
'.m': 'MATLAB/Objective-C',
|
113
|
+
'.vb': 'Visual Basic',
|
114
|
+
'.dart': 'Dart',
|
115
|
+
'.jl': 'Julia',
|
116
|
+
'.sh': 'Shell',
|
117
|
+
'.bash': 'Bash',
|
118
|
+
'.zsh': 'Zsh',
|
119
|
+
'.ps1': 'PowerShell',
|
120
|
+
'.html': 'HTML',
|
121
|
+
'.htm': 'HTML',
|
122
|
+
'.xml': 'XML',
|
123
|
+
'.css': 'CSS',
|
124
|
+
'.scss': 'SCSS',
|
125
|
+
'.sass': 'Sass',
|
126
|
+
'.less': 'Less',
|
127
|
+
'.json': 'JSON',
|
128
|
+
'.yaml': 'YAML',
|
129
|
+
'.yml': 'YAML',
|
130
|
+
'.toml': 'TOML',
|
131
|
+
'.ini': 'INI',
|
132
|
+
'.cfg': 'Config',
|
133
|
+
'.conf': 'Config',
|
134
|
+
'.sql': 'SQL',
|
135
|
+
'.md': 'Markdown',
|
136
|
+
'.rst': 'reStructuredText',
|
137
|
+
'.tex': 'LaTeX'
|
138
|
+
}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
"""
|
2
|
+
エラーメッセージ処理のユーティリティ
|
3
|
+
重複したエラーメッセージ処理ロジックを統一
|
4
|
+
"""
|
5
|
+
from typing import Optional
|
6
|
+
from ..tr import tr
|
7
|
+
|
8
|
+
|
9
|
+
def get_user_friendly_error_message(error: Exception, context: str = "") -> str:
|
10
|
+
"""
|
11
|
+
技術的なエラーをユーザーフレンドリーなメッセージに変換
|
12
|
+
|
13
|
+
Args:
|
14
|
+
error: 発生したException
|
15
|
+
context: エラーの文脈(例: "generating response", "loading model")
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
ユーザー向けのエラーメッセージ
|
19
|
+
"""
|
20
|
+
error_str = str(error).lower()
|
21
|
+
|
22
|
+
# エラーメッセージのマッピング
|
23
|
+
ERROR_MAPPINGS = {
|
24
|
+
# ネットワーク関連
|
25
|
+
"connection": tr("Connection error. Please check your network and API settings."),
|
26
|
+
"urlopen": tr("Cannot connect to server. Please check if the service is running."),
|
27
|
+
"timeout": tr("Request timed out. The server may be busy or unreachable."),
|
28
|
+
"refused": tr("Connection refused. Please check if the service is running."),
|
29
|
+
|
30
|
+
# 認証関連
|
31
|
+
"api key": tr("API key error. Please check your API key in settings."),
|
32
|
+
"401": tr("Invalid API key. Please check your API key."),
|
33
|
+
"403": tr("Access denied. Your API key may not have the required permissions."),
|
34
|
+
"invalid_api_key": tr("Invalid API key. Please check your API key in settings."),
|
35
|
+
|
36
|
+
# レート制限
|
37
|
+
"rate limit": tr("Rate limit exceeded. Please try again later."),
|
38
|
+
"429": tr("Too many requests. Please try again later."),
|
39
|
+
|
40
|
+
# モデル関連
|
41
|
+
"model": tr("Model error. The selected model may not be available."),
|
42
|
+
"model not found": tr("Model not found. Please check the model name."),
|
43
|
+
"not supported": tr("This model is not supported."),
|
44
|
+
|
45
|
+
# ファイル関連
|
46
|
+
"file not found": tr("File not found. Please check the file path."),
|
47
|
+
"permission denied": tr("Permission denied. Cannot access the file."),
|
48
|
+
|
49
|
+
# メモリ関連
|
50
|
+
"out of memory": tr("Out of memory. Try using a smaller model or reducing context size."),
|
51
|
+
"memory": tr("Memory error. Try reducing the context size."),
|
52
|
+
}
|
53
|
+
|
54
|
+
# エラーメッセージをチェック
|
55
|
+
for key, message in ERROR_MAPPINGS.items():
|
56
|
+
if key in error_str:
|
57
|
+
return message
|
58
|
+
|
59
|
+
# デフォルトメッセージ
|
60
|
+
if context:
|
61
|
+
return f"{tr('Error')} {tr(context)}: {str(error)}"
|
62
|
+
else:
|
63
|
+
return f"{tr('Error')}: {str(error)}"
|
64
|
+
|
65
|
+
|
66
|
+
def format_api_error(provider: str, error: Exception) -> str:
|
67
|
+
"""
|
68
|
+
API プロバイダー固有のエラーメッセージをフォーマット
|
69
|
+
|
70
|
+
Args:
|
71
|
+
provider: プロバイダー名 ("chatgpt", "ollama", "openrouter", etc.)
|
72
|
+
error: 発生したException
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
フォーマットされたエラーメッセージ
|
76
|
+
"""
|
77
|
+
base_message = get_user_friendly_error_message(error)
|
78
|
+
|
79
|
+
# プロバイダー固有の追加情報
|
80
|
+
provider_tips = {
|
81
|
+
"chatgpt": tr("Make sure you have a valid OpenAI API key."),
|
82
|
+
"ollama": tr("Make sure Ollama is running on the specified host and port."),
|
83
|
+
"lmstudio": tr("Make sure LM Studio server is running."),
|
84
|
+
"openrouter": tr("Check your OpenRouter API key and credits."),
|
85
|
+
"local": tr("Make sure the model file exists and is not corrupted.")
|
86
|
+
}
|
87
|
+
|
88
|
+
tip = provider_tips.get(provider, "")
|
89
|
+
if tip:
|
90
|
+
return f"{base_message}\n{tip}"
|
91
|
+
|
92
|
+
return base_message
|
@@ -0,0 +1,310 @@
|
|
1
|
+
"""
|
2
|
+
統一エラーハンドリングモジュール
|
3
|
+
重複していたエラーハンドリング機能を統合
|
4
|
+
"""
|
5
|
+
import logging
|
6
|
+
import traceback
|
7
|
+
import functools
|
8
|
+
import time
|
9
|
+
from typing import Optional, Callable, Any, Dict, Tuple
|
10
|
+
from contextlib import contextmanager
|
11
|
+
from ..i18n import tr
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class ErrorContext:
|
17
|
+
"""エラーコンテキスト情報を保持するクラス"""
|
18
|
+
|
19
|
+
def __init__(self, operation: str, details: Optional[Dict[str, Any]] = None):
|
20
|
+
self.operation = operation
|
21
|
+
self.details = details or {}
|
22
|
+
self.timestamp = time.time()
|
23
|
+
self.error: Optional[Exception] = None
|
24
|
+
self.traceback: Optional[str] = None
|
25
|
+
|
26
|
+
def capture_error(self, error: Exception):
|
27
|
+
"""エラー情報をキャプチャ"""
|
28
|
+
self.error = error
|
29
|
+
self.traceback = traceback.format_exc()
|
30
|
+
|
31
|
+
def to_dict(self) -> Dict[str, Any]:
|
32
|
+
return {
|
33
|
+
"operation": self.operation,
|
34
|
+
"details": self.details,
|
35
|
+
"timestamp": self.timestamp,
|
36
|
+
"error": str(self.error) if self.error else None,
|
37
|
+
"traceback": self.traceback
|
38
|
+
}
|
39
|
+
|
40
|
+
def get_user_message(self) -> str:
|
41
|
+
"""ユーザー向けのエラーメッセージを生成"""
|
42
|
+
if not self.error:
|
43
|
+
return tr("Unknown error occurred")
|
44
|
+
|
45
|
+
error_str = str(self.error).lower()
|
46
|
+
error_type = type(self.error).__name__
|
47
|
+
|
48
|
+
# 特定のエラータイプに対するメッセージ
|
49
|
+
if isinstance(self.error, FileNotFoundError):
|
50
|
+
return tr("File not found during {}: {}").format(self.operation, str(self.error))
|
51
|
+
elif isinstance(self.error, PermissionError):
|
52
|
+
return tr("Permission denied during {}: {}").format(self.operation, str(self.error))
|
53
|
+
elif isinstance(self.error, ConnectionError) or "connection" in error_str or "urlopen" in error_str:
|
54
|
+
return tr("Connection failed during {}: {}").format(self.operation, str(self.error))
|
55
|
+
elif isinstance(self.error, TimeoutError) or "timeout" in error_str:
|
56
|
+
return tr("Operation timed out during {}: {}").format(self.operation, str(self.error))
|
57
|
+
elif isinstance(self.error, ValueError):
|
58
|
+
return tr("Invalid value during {}: {}").format(self.operation, str(self.error))
|
59
|
+
elif isinstance(self.error, ImportError):
|
60
|
+
return tr("Missing dependency during {}: {}").format(self.operation, str(self.error))
|
61
|
+
elif isinstance(self.error, MemoryError) or "memory" in error_str or "oom" in error_str:
|
62
|
+
return tr("Out of memory. Try using a smaller model or reducing context size.")
|
63
|
+
else:
|
64
|
+
# その他のエラー
|
65
|
+
return tr("Error during {}: {}").format(self.operation, str(self.error))
|
66
|
+
|
67
|
+
|
68
|
+
def log_error_with_context(
|
69
|
+
error: Exception,
|
70
|
+
context: ErrorContext,
|
71
|
+
user_message: Optional[str] = None,
|
72
|
+
log_level: int = logging.ERROR
|
73
|
+
) -> str:
|
74
|
+
"""
|
75
|
+
エラーを詳細なコンテキスト情報と共にログに記録
|
76
|
+
|
77
|
+
Args:
|
78
|
+
error: 発生したエラー
|
79
|
+
context: エラーコンテキスト
|
80
|
+
user_message: ユーザーに表示するメッセージ(None の場合は自動生成)
|
81
|
+
log_level: ログレベル
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
ユーザーに表示するメッセージ
|
85
|
+
"""
|
86
|
+
# エラー情報をキャプチャ
|
87
|
+
context.capture_error(error)
|
88
|
+
|
89
|
+
# ログに記録
|
90
|
+
logger.log(
|
91
|
+
log_level,
|
92
|
+
f"Error in {context.operation}: {type(error).__name__} - {str(error)}",
|
93
|
+
extra={"error_context": context.to_dict()}
|
94
|
+
)
|
95
|
+
|
96
|
+
if context.traceback:
|
97
|
+
logger.debug(f"Stack trace:\n{context.traceback}")
|
98
|
+
|
99
|
+
# ユーザー向けメッセージを生成
|
100
|
+
if user_message is None:
|
101
|
+
user_message = context.get_user_message()
|
102
|
+
|
103
|
+
return user_message
|
104
|
+
|
105
|
+
|
106
|
+
def handle_api_error(error: Exception, provider: str) -> str:
|
107
|
+
"""
|
108
|
+
API エラーを処理してユーザーフレンドリーなメッセージを返す
|
109
|
+
|
110
|
+
Args:
|
111
|
+
error: 発生したエラー
|
112
|
+
provider: APIプロバイダー名
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
ユーザー向けのエラーメッセージ
|
116
|
+
"""
|
117
|
+
error_str = str(error).lower()
|
118
|
+
|
119
|
+
# HTTPステータスコードのチェック
|
120
|
+
if "401" in error_str or "unauthorized" in error_str:
|
121
|
+
return tr("API key error. Please check your API key in settings.")
|
122
|
+
elif "403" in error_str:
|
123
|
+
return tr(f"Access forbidden. Please check your {provider} account permissions.")
|
124
|
+
elif "404" in error_str or "not found" in error_str:
|
125
|
+
return tr(f"API endpoint not found. Please check your {provider} configuration.")
|
126
|
+
elif "429" in error_str or "rate limit" in error_str:
|
127
|
+
return tr(f"Rate limit exceeded for {provider}. Please wait a moment and try again.")
|
128
|
+
elif any(code in error_str for code in ["500", "502", "503"]):
|
129
|
+
return tr(f"{provider} server error. Please try again later.")
|
130
|
+
|
131
|
+
# 接続エラー
|
132
|
+
if "connection" in error_str or "urlopen" in error_str:
|
133
|
+
return tr("Connection failed. Please check your internet connection or server settings.")
|
134
|
+
|
135
|
+
# タイムアウト
|
136
|
+
if "timeout" in error_str:
|
137
|
+
return tr("Request timed out. Please try again.")
|
138
|
+
|
139
|
+
# モデルエラー
|
140
|
+
if "model" in error_str:
|
141
|
+
return tr("Model error. Please check if the model is properly loaded.")
|
142
|
+
|
143
|
+
# デフォルト
|
144
|
+
return tr(f"{provider} error: {str(error)}")
|
145
|
+
|
146
|
+
|
147
|
+
def with_error_handling(
|
148
|
+
operation: str,
|
149
|
+
show_user_message: bool = True,
|
150
|
+
default_return: Any = None,
|
151
|
+
details: Optional[Dict[str, Any]] = None
|
152
|
+
):
|
153
|
+
"""
|
154
|
+
エラーハンドリングを追加するデコレーター
|
155
|
+
|
156
|
+
Args:
|
157
|
+
operation: 操作の説明
|
158
|
+
show_user_message: ユーザーにメッセージを表示するか
|
159
|
+
default_return: エラー時のデフォルト戻り値
|
160
|
+
details: 追加のコンテキスト情報
|
161
|
+
"""
|
162
|
+
def decorator(func: Callable) -> Callable:
|
163
|
+
@functools.wraps(func)
|
164
|
+
def wrapper(*args, **kwargs):
|
165
|
+
context_details = details or {}
|
166
|
+
context_details.update({
|
167
|
+
"function": func.__name__,
|
168
|
+
"args": str(args)[:200], # 長すぎる場合は切り詰め
|
169
|
+
"kwargs": str(kwargs)[:200]
|
170
|
+
})
|
171
|
+
|
172
|
+
context = ErrorContext(operation=operation, details=context_details)
|
173
|
+
|
174
|
+
try:
|
175
|
+
return func(*args, **kwargs)
|
176
|
+
except Exception as e:
|
177
|
+
user_message = log_error_with_context(e, context)
|
178
|
+
|
179
|
+
if show_user_message:
|
180
|
+
# UIスレッドでメッセージボックスを表示
|
181
|
+
try:
|
182
|
+
from tkinter import messagebox
|
183
|
+
messagebox.showerror(tr("Error"), user_message)
|
184
|
+
except:
|
185
|
+
# UIが利用できない場合はログのみ
|
186
|
+
pass
|
187
|
+
|
188
|
+
return default_return
|
189
|
+
|
190
|
+
return wrapper
|
191
|
+
return decorator
|
192
|
+
|
193
|
+
|
194
|
+
@contextmanager
|
195
|
+
def error_context(operation: str, **details):
|
196
|
+
"""
|
197
|
+
エラーコンテキストを提供するコンテキストマネージャー
|
198
|
+
|
199
|
+
使用例:
|
200
|
+
with error_context("loading model", model_path=path):
|
201
|
+
# モデルロード処理
|
202
|
+
"""
|
203
|
+
context = ErrorContext(operation, details)
|
204
|
+
try:
|
205
|
+
yield context
|
206
|
+
except Exception as e:
|
207
|
+
log_error_with_context(e, context)
|
208
|
+
raise
|
209
|
+
|
210
|
+
|
211
|
+
def retry_operation(
|
212
|
+
func: Callable,
|
213
|
+
max_attempts: int = 3,
|
214
|
+
delay: float = 1.0,
|
215
|
+
backoff: float = 2.0,
|
216
|
+
exceptions: Tuple[type, ...] = (Exception,),
|
217
|
+
operation_name: Optional[str] = None
|
218
|
+
) -> Any:
|
219
|
+
"""
|
220
|
+
操作をリトライ付きで実行
|
221
|
+
|
222
|
+
Args:
|
223
|
+
func: 実行する関数
|
224
|
+
max_attempts: 最大試行回数
|
225
|
+
delay: 初回リトライまでの待機時間(秒)
|
226
|
+
backoff: リトライごとの待機時間の倍率
|
227
|
+
exceptions: リトライ対象の例外タプル
|
228
|
+
operation_name: 操作名(ログ用)
|
229
|
+
|
230
|
+
Returns:
|
231
|
+
関数の実行結果
|
232
|
+
|
233
|
+
Raises:
|
234
|
+
最後の試行でも失敗した場合は例外を再発生
|
235
|
+
"""
|
236
|
+
operation_name = operation_name or func.__name__
|
237
|
+
last_error = None
|
238
|
+
current_delay = delay
|
239
|
+
|
240
|
+
for attempt in range(max_attempts):
|
241
|
+
try:
|
242
|
+
return func()
|
243
|
+
except exceptions as e:
|
244
|
+
last_error = e
|
245
|
+
context = ErrorContext(
|
246
|
+
f"{operation_name} (attempt {attempt + 1}/{max_attempts})",
|
247
|
+
{"error": str(e)}
|
248
|
+
)
|
249
|
+
|
250
|
+
if attempt < max_attempts - 1:
|
251
|
+
logger.info(
|
252
|
+
f"Retrying {operation_name} after {current_delay}s due to: {e}"
|
253
|
+
)
|
254
|
+
time.sleep(current_delay)
|
255
|
+
current_delay *= backoff
|
256
|
+
else:
|
257
|
+
log_error_with_context(e, context)
|
258
|
+
|
259
|
+
raise last_error
|
260
|
+
|
261
|
+
|
262
|
+
def retry_decorator(
|
263
|
+
max_attempts: int = 3,
|
264
|
+
delay: float = 1.0,
|
265
|
+
backoff: float = 2.0,
|
266
|
+
exceptions: Tuple[type, ...] = (ConnectionError, TimeoutError)
|
267
|
+
):
|
268
|
+
"""
|
269
|
+
リトライ機能を追加するデコレーター
|
270
|
+
|
271
|
+
Args:
|
272
|
+
max_attempts: 最大試行回数
|
273
|
+
delay: 初回リトライまでの待機時間(秒)
|
274
|
+
backoff: リトライごとの待機時間の倍率
|
275
|
+
exceptions: リトライ対象の例外タプル
|
276
|
+
"""
|
277
|
+
def decorator(func: Callable) -> Callable:
|
278
|
+
@functools.wraps(func)
|
279
|
+
def wrapper(*args, **kwargs):
|
280
|
+
return retry_operation(
|
281
|
+
lambda: func(*args, **kwargs),
|
282
|
+
max_attempts=max_attempts,
|
283
|
+
delay=delay,
|
284
|
+
backoff=backoff,
|
285
|
+
exceptions=exceptions,
|
286
|
+
operation_name=func.__name__
|
287
|
+
)
|
288
|
+
return wrapper
|
289
|
+
return decorator
|
290
|
+
|
291
|
+
|
292
|
+
def safe_execute(func: Callable, default_value: Any = None, log_errors: bool = True):
|
293
|
+
"""
|
294
|
+
関数を安全に実行し、エラー時はデフォルト値を返す
|
295
|
+
|
296
|
+
Args:
|
297
|
+
func: 実行する関数
|
298
|
+
default_value: エラー時のデフォルト値
|
299
|
+
log_errors: エラーをログに記録するか
|
300
|
+
|
301
|
+
Returns:
|
302
|
+
関数の実行結果またはデフォルト値
|
303
|
+
"""
|
304
|
+
try:
|
305
|
+
return func()
|
306
|
+
except Exception as e:
|
307
|
+
if log_errors:
|
308
|
+
context = ErrorContext("safe_execute", {"function": str(func)})
|
309
|
+
log_error_with_context(e, context, log_level=logging.WARNING)
|
310
|
+
return default_value
|