Coverage for railway / core / errors.py: 55%
114 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:06 +0900
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 00:06 +0900
1"""Custom error types for Railway Framework."""
3from typing import Any, Dict, Optional
6class RailwayError(Exception):
7 """Base exception for all Railway Framework errors."""
9 def __init__(
10 self,
11 message: str,
12 *,
13 code: Optional[str] = None,
14 hint: Optional[str] = None,
15 retryable: bool = False,
16 ):
17 super().__init__(message)
18 self.message = message
19 self.code = code
20 self.hint = hint
21 self.retryable = retryable
23 def full_message(self) -> str:
24 """Get full formatted error message."""
25 parts = []
27 if self.code:
28 parts.append(f"[{self.code}]")
30 parts.append(self.message)
32 if self.hint:
33 parts.append(f"\nヒント: {self.hint}")
35 return " ".join(parts)
37 def to_dict(self) -> Dict[str, Any]:
38 """Convert error to dictionary."""
39 return {
40 "type": self.__class__.__name__,
41 "message": self.message,
42 "code": self.code,
43 "hint": self.hint,
44 "retryable": self.retryable,
45 }
48class ConfigurationError(RailwayError):
49 """Error related to configuration issues."""
51 def __init__(
52 self,
53 message: str,
54 *,
55 code: Optional[str] = None,
56 hint: Optional[str] = None,
57 config_key: Optional[str] = None,
58 ):
59 if hint is None: 59 ↛ 62line 59 didn't jump to line 62 because the condition on line 59 was always true
60 hint = "設定ファイル(config/*.yaml)または環境変数を確認してください。"
62 super().__init__(message, code=code, hint=hint, retryable=False)
63 self.config_key = config_key
65 def to_dict(self) -> Dict[str, Any]:
66 d = super().to_dict()
67 d["config_key"] = self.config_key
68 return d
71class NodeError(RailwayError):
72 """Error that occurred in a node."""
74 def __init__(
75 self,
76 message: str,
77 *,
78 code: Optional[str] = None,
79 hint: Optional[str] = None,
80 retryable: bool = True,
81 node_name: Optional[str] = None,
82 original_error: Optional[Exception] = None,
83 ):
84 super().__init__(message, code=code, hint=hint, retryable=retryable)
85 self.node_name = node_name
86 self.original_error = original_error
88 def full_message(self) -> str:
89 """Get full formatted error message with node info."""
90 parts = []
92 if self.code: 92 ↛ 95line 92 didn't jump to line 95 because the condition on line 92 was always true
93 parts.append(f"[{self.code}]")
95 if self.node_name: 95 ↛ 98line 95 didn't jump to line 98 because the condition on line 95 was always true
96 parts.append(f"[{self.node_name}]")
98 parts.append(self.message)
100 if self.hint: 100 ↛ 103line 100 didn't jump to line 103 because the condition on line 100 was always true
101 parts.append(f"\nヒント: {self.hint}")
103 return " ".join(parts)
105 def to_dict(self) -> Dict[str, Any]:
106 d = super().to_dict()
107 d["node_name"] = self.node_name
108 if self.original_error:
109 d["original_error"] = {
110 "type": type(self.original_error).__name__,
111 "message": str(self.original_error),
112 }
113 return d
116class PipelineError(RailwayError):
117 """Error that occurred in a pipeline."""
119 def __init__(
120 self,
121 message: str,
122 *,
123 code: Optional[str] = None,
124 hint: Optional[str] = None,
125 step_number: Optional[int] = None,
126 step_name: Optional[str] = None,
127 total_steps: Optional[int] = None,
128 original_error: Optional[Exception] = None,
129 ):
130 super().__init__(message, code=code, hint=hint, retryable=False)
131 self.step_number = step_number
132 self.step_name = step_name
133 self.total_steps = total_steps
134 self.original_error = original_error
136 @property
137 def remaining_steps(self) -> Optional[int]:
138 """Get number of remaining steps after failure."""
139 if self.step_number is not None and self.total_steps is not None: 139 ↛ 141line 139 didn't jump to line 141 because the condition on line 139 was always true
140 return self.total_steps - self.step_number
141 return None
143 def full_message(self) -> str:
144 """Get full formatted error message with pipeline info."""
145 parts = []
147 if self.code:
148 parts.append(f"[{self.code}]")
150 if self.step_name and self.step_number:
151 parts.append(f"Step {self.step_number} ({self.step_name}):")
153 parts.append(self.message)
155 if self.remaining_steps is not None and self.remaining_steps > 0:
156 parts.append(f"(残り {self.remaining_steps} ステップはスキップされました)")
158 if self.hint:
159 parts.append(f"\nヒント: {self.hint}")
161 return " ".join(parts)
163 def to_dict(self) -> Dict[str, Any]:
164 d = super().to_dict()
165 d["step_number"] = self.step_number
166 d["step_name"] = self.step_name
167 d["total_steps"] = self.total_steps
168 d["remaining_steps"] = self.remaining_steps
169 return d
172class NetworkError(RailwayError):
173 """Error related to network operations."""
175 def __init__(
176 self,
177 message: str,
178 *,
179 code: Optional[str] = None,
180 hint: Optional[str] = None,
181 url: Optional[str] = None,
182 status_code: Optional[int] = None,
183 ):
184 if hint is None: 184 ↛ 187line 184 didn't jump to line 187 because the condition on line 184 was always true
185 hint = "ネットワーク接続を確認してください。APIエンドポイントが正しいか確認してください。"
187 super().__init__(message, code=code, hint=hint, retryable=True)
188 self.url = url
189 self.status_code = status_code
191 def to_dict(self) -> Dict[str, Any]:
192 d = super().to_dict()
193 d["url"] = self.url
194 d["status_code"] = self.status_code
195 return d
198class ValidationError(RailwayError):
199 """Error related to data validation."""
201 def __init__(
202 self,
203 message: str,
204 *,
205 code: Optional[str] = None,
206 hint: Optional[str] = None,
207 field: Optional[str] = None,
208 value: Any = None,
209 ):
210 if hint is None: 210 ↛ 213line 210 didn't jump to line 213 because the condition on line 210 was always true
211 hint = "入力データの形式を確認してください。"
213 super().__init__(message, code=code, hint=hint, retryable=False)
214 self.field = field
215 self.value = value
217 def to_dict(self) -> Dict[str, Any]:
218 d = super().to_dict()
219 d["field"] = self.field
220 d["value"] = repr(self.value) if self.value is not None else None
221 return d
224class RailwayTimeoutError(RailwayError):
225 """Error when operation times out."""
227 def __init__(
228 self,
229 message: str,
230 *,
231 code: Optional[str] = None,
232 hint: Optional[str] = None,
233 timeout_seconds: Optional[float] = None,
234 ):
235 if hint is None:
236 hint = "タイムアウト値を増やすか、処理を分割してください。"
238 super().__init__(message, code=code, hint=hint, retryable=True)
239 self.timeout_seconds = timeout_seconds
241 def to_dict(self) -> Dict[str, Any]:
242 d = super().to_dict()
243 d["timeout_seconds"] = self.timeout_seconds
244 return d