Skip to content

Commit 55205fc

Browse files
committed
feat: enhance ModuleError with retryable, AI guidance, user fixability, and suggestion attributes, including a new serialization method and corresponding tests.
1 parent da8cc90 commit 55205fc

File tree

3 files changed

+380
-3
lines changed

3 files changed

+380
-3
lines changed

src/apcore/errors.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,25 @@
4040
"ErrorCodes",
4141
]
4242

43+
_UNSET: Any = object()
44+
4345

4446
class ModuleError(Exception):
4547
"""Base error for all apcore framework errors."""
4648

49+
_default_retryable: bool | None = None
50+
4751
def __init__(
4852
self,
4953
code: str,
5054
message: str,
5155
details: dict[str, Any] | None = None,
5256
cause: Exception | None = None,
5357
trace_id: str | None = None,
58+
retryable: Any = _UNSET,
59+
ai_guidance: str | None = None,
60+
user_fixable: bool | None = None,
61+
suggestion: str | None = None,
5462
) -> None:
5563
super().__init__(message)
5664
self.code = code
@@ -59,14 +67,43 @@ def __init__(
5967
self.cause = cause
6068
self.trace_id = trace_id
6169
self.timestamp = datetime.now(timezone.utc).isoformat()
70+
self.retryable = self._default_retryable if retryable is _UNSET else retryable
71+
self.ai_guidance = ai_guidance
72+
self.user_fixable = user_fixable
73+
self.suggestion = suggestion
6274

6375
def __str__(self) -> str:
6476
return f"[{self.code}] {self.message}"
6577

78+
def to_dict(self) -> dict[str, Any]:
79+
"""Serialize to dict with sparse output (null fields omitted)."""
80+
d: dict[str, Any] = {
81+
"code": self.code,
82+
"message": self.message,
83+
}
84+
if self.details:
85+
d["details"] = self.details
86+
if self.cause is not None:
87+
d["cause"] = str(self.cause)
88+
if self.trace_id is not None:
89+
d["trace_id"] = self.trace_id
90+
d["timestamp"] = self.timestamp
91+
if self.retryable is not None:
92+
d["retryable"] = self.retryable
93+
if self.ai_guidance is not None:
94+
d["ai_guidance"] = self.ai_guidance
95+
if self.user_fixable is not None:
96+
d["user_fixable"] = self.user_fixable
97+
if self.suggestion is not None:
98+
d["suggestion"] = self.suggestion
99+
return d
100+
66101

67102
class ConfigNotFoundError(ModuleError):
68103
"""Raised when a configuration file cannot be found."""
69104

105+
_default_retryable: bool | None = False
106+
70107
def __init__(self, config_path: str, **kwargs: Any) -> None:
71108
super().__init__(
72109
code="CONFIG_NOT_FOUND",
@@ -79,20 +116,26 @@ def __init__(self, config_path: str, **kwargs: Any) -> None:
79116
class ConfigError(ModuleError):
80117
"""Raised when configuration is invalid."""
81118

119+
_default_retryable: bool | None = False
120+
82121
def __init__(self, message: str, **kwargs: Any) -> None:
83122
super().__init__(code="CONFIG_INVALID", message=message, **kwargs)
84123

85124

86125
class ACLRuleError(ModuleError):
87126
"""Raised when an ACL rule is invalid."""
88127

128+
_default_retryable: bool | None = False
129+
89130
def __init__(self, message: str, **kwargs: Any) -> None:
90131
super().__init__(code="ACL_RULE_ERROR", message=message, **kwargs)
91132

92133

93134
class ACLDeniedError(ModuleError):
94135
"""Raised when ACL denies access."""
95136

137+
_default_retryable: bool | None = False
138+
96139
def __init__(self, caller_id: str | None, target_id: str, **kwargs: Any) -> None:
97140
super().__init__(
98141
code="ACL_DENIED",
@@ -120,6 +163,8 @@ class ApprovalError(ModuleError):
120163
``apcore.approval`` where ``ApprovalResult`` is defined.
121164
"""
122165

166+
_default_retryable: bool | None = False
167+
123168
def __init__(
124169
self,
125170
code: str,
@@ -149,6 +194,8 @@ def reason(self) -> str | None:
149194
class ApprovalDeniedError(ApprovalError):
150195
"""Raised when an approval handler rejects the request."""
151196

197+
_default_retryable: bool | None = False
198+
152199
def __init__(self, result: Any, module_id: str = "", **kwargs: Any) -> None:
153200
reason = getattr(result, "reason", None) or ""
154201
msg = f"Approval denied for module '{module_id}'"
@@ -166,6 +213,8 @@ def __init__(self, result: Any, module_id: str = "", **kwargs: Any) -> None:
166213
class ApprovalTimeoutError(ApprovalError):
167214
"""Raised when an approval request times out."""
168215

216+
_default_retryable: bool | None = True
217+
169218
def __init__(self, result: Any, module_id: str = "", **kwargs: Any) -> None:
170219
super().__init__(
171220
code="APPROVAL_TIMEOUT",
@@ -179,6 +228,8 @@ def __init__(self, result: Any, module_id: str = "", **kwargs: Any) -> None:
179228
class ApprovalPendingError(ApprovalError):
180229
"""Raised when an approval is pending async resolution (Phase B)."""
181230

231+
_default_retryable: bool | None = False
232+
182233
def __init__(self, result: Any, module_id: str = "", **kwargs: Any) -> None:
183234
approval_id = getattr(result, "approval_id", None)
184235
super().__init__(
@@ -199,6 +250,8 @@ def approval_id(self) -> str | None:
199250
class ModuleNotFoundError(ModuleError):
200251
"""Raised when a module cannot be found."""
201252

253+
_default_retryable: bool | None = False
254+
202255
def __init__(self, module_id: str, **kwargs: Any) -> None:
203256
super().__init__(
204257
code="MODULE_NOT_FOUND",
@@ -211,6 +264,8 @@ def __init__(self, module_id: str, **kwargs: Any) -> None:
211264
class ModuleTimeoutError(ModuleError):
212265
"""Raised when module execution exceeds timeout."""
213266

267+
_default_retryable: bool | None = True
268+
214269
def __init__(self, module_id: str, timeout_ms: int, **kwargs: Any) -> None:
215270
super().__init__(
216271
code="MODULE_TIMEOUT",
@@ -233,6 +288,8 @@ def timeout_ms(self) -> int:
233288
class SchemaValidationError(ModuleError):
234289
"""Raised when schema validation fails."""
235290

291+
_default_retryable: bool | None = False
292+
236293
def __init__(
237294
self,
238295
message: str = "Schema validation failed",
@@ -250,6 +307,8 @@ def __init__(
250307
class SchemaNotFoundError(ModuleError):
251308
"""Raised when a schema file or reference target cannot be found."""
252309

310+
_default_retryable: bool | None = False
311+
253312
def __init__(self, schema_id: str, **kwargs: Any) -> None:
254313
super().__init__(
255314
code="SCHEMA_NOT_FOUND",
@@ -262,13 +321,17 @@ def __init__(self, schema_id: str, **kwargs: Any) -> None:
262321
class SchemaParseError(ModuleError):
263322
"""Raised when a schema file has invalid syntax."""
264323

324+
_default_retryable: bool | None = False
325+
265326
def __init__(self, message: str, **kwargs: Any) -> None:
266327
super().__init__(code="SCHEMA_PARSE_ERROR", message=message, **kwargs)
267328

268329

269330
class SchemaCircularRefError(ModuleError):
270331
"""Raised when circular $ref references are detected."""
271332

333+
_default_retryable: bool | None = False
334+
272335
def __init__(self, ref_path: str, **kwargs: Any) -> None:
273336
super().__init__(
274337
code="SCHEMA_CIRCULAR_REF",
@@ -281,6 +344,8 @@ def __init__(self, ref_path: str, **kwargs: Any) -> None:
281344
class CallDepthExceededError(ModuleError):
282345
"""Raised when call chain exceeds maximum depth."""
283346

347+
_default_retryable: bool | None = False
348+
284349
def __init__(self, depth: int, max_depth: int, call_chain: list[str], **kwargs: Any) -> None:
285350
super().__init__(
286351
code="CALL_DEPTH_EXCEEDED",
@@ -303,6 +368,8 @@ def max_depth(self) -> int:
303368
class CircularCallError(ModuleError):
304369
"""Raised when a circular call is detected."""
305370

371+
_default_retryable: bool | None = False
372+
306373
def __init__(self, module_id: str, call_chain: list[str], **kwargs: Any) -> None:
307374
super().__init__(
308375
code="CIRCULAR_CALL",
@@ -320,6 +387,8 @@ def module_id(self) -> str:
320387
class CallFrequencyExceededError(ModuleError):
321388
"""Raised when a module is called too many times."""
322389

390+
_default_retryable: bool | None = False
391+
323392
def __init__(
324393
self,
325394
module_id: str,
@@ -359,13 +428,17 @@ def max_repeat(self) -> int:
359428
class InvalidInputError(ModuleError):
360429
"""Raised for invalid input."""
361430

431+
_default_retryable: bool | None = False
432+
362433
def __init__(self, message: str = "Invalid input", **kwargs: Any) -> None:
363434
super().__init__(code="GENERAL_INVALID_INPUT", message=message, **kwargs)
364435

365436

366437
class FuncMissingTypeHintError(ModuleError):
367438
"""Raised when a function parameter has no type annotation or a forward reference cannot be resolved."""
368439

440+
_default_retryable: bool | None = False
441+
369442
def __init__(self, *, function_name: str, parameter_name: str, **kwargs: Any) -> None:
370443
super().__init__(
371444
code="FUNC_MISSING_TYPE_HINT",
@@ -381,6 +454,8 @@ def __init__(self, *, function_name: str, parameter_name: str, **kwargs: Any) ->
381454
class FuncMissingReturnTypeError(ModuleError):
382455
"""Raised when a function has no return type annotation."""
383456

457+
_default_retryable: bool | None = False
458+
384459
def __init__(self, *, function_name: str, **kwargs: Any) -> None:
385460
super().__init__(
386461
code="FUNC_MISSING_RETURN_TYPE",
@@ -393,6 +468,8 @@ def __init__(self, *, function_name: str, **kwargs: Any) -> None:
393468
class BindingInvalidTargetError(ModuleError):
394469
"""Raised when a binding target string does not contain a ':' separator."""
395470

471+
_default_retryable: bool | None = False
472+
396473
def __init__(self, *, target: str, **kwargs: Any) -> None:
397474
super().__init__(
398475
code="BINDING_INVALID_TARGET",
@@ -405,6 +482,8 @@ def __init__(self, *, target: str, **kwargs: Any) -> None:
405482
class BindingModuleNotFoundError(ModuleError):
406483
"""Raised when a binding target module cannot be imported."""
407484

485+
_default_retryable: bool | None = False
486+
408487
def __init__(self, *, module_path: str, **kwargs: Any) -> None:
409488
super().__init__(
410489
code="BINDING_MODULE_NOT_FOUND",
@@ -417,6 +496,8 @@ def __init__(self, *, module_path: str, **kwargs: Any) -> None:
417496
class BindingCallableNotFoundError(ModuleError):
418497
"""Raised when a callable cannot be found in the target module."""
419498

499+
_default_retryable: bool | None = False
500+
420501
def __init__(self, *, callable_name: str, module_path: str, **kwargs: Any) -> None:
421502
super().__init__(
422503
code="BINDING_CALLABLE_NOT_FOUND",
@@ -429,6 +510,8 @@ def __init__(self, *, callable_name: str, module_path: str, **kwargs: Any) -> No
429510
class BindingNotCallableError(ModuleError):
430511
"""Raised when a resolved binding target is not callable."""
431512

513+
_default_retryable: bool | None = False
514+
432515
def __init__(self, *, target: str, **kwargs: Any) -> None:
433516
super().__init__(
434517
code="BINDING_NOT_CALLABLE",
@@ -441,6 +524,8 @@ def __init__(self, *, target: str, **kwargs: Any) -> None:
441524
class BindingSchemaMissingError(ModuleError):
442525
"""Raised when no schema is provided and auto-generation from type hints fails."""
443526

527+
_default_retryable: bool | None = False
528+
444529
def __init__(self, *, target: str, **kwargs: Any) -> None:
445530
super().__init__(
446531
code="BINDING_SCHEMA_MISSING",
@@ -453,6 +538,8 @@ def __init__(self, *, target: str, **kwargs: Any) -> None:
453538
class BindingFileInvalidError(ModuleError):
454539
"""Raised when a binding file has parse errors, missing required fields, or is empty."""
455540

541+
_default_retryable: bool | None = False
542+
456543
def __init__(self, *, file_path: str, reason: str, **kwargs: Any) -> None:
457544
super().__init__(
458545
code="BINDING_FILE_INVALID",
@@ -465,6 +552,8 @@ def __init__(self, *, file_path: str, reason: str, **kwargs: Any) -> None:
465552
class CircularDependencyError(ModuleError):
466553
"""Raised when circular dependencies are detected among modules."""
467554

555+
_default_retryable: bool | None = False
556+
468557
def __init__(self, cycle_path: list[str], **kwargs: Any) -> None:
469558
super().__init__(
470559
code="CIRCULAR_DEPENDENCY",
@@ -477,6 +566,8 @@ def __init__(self, cycle_path: list[str], **kwargs: Any) -> None:
477566
class ModuleLoadError(ModuleError):
478567
"""Raised when a module file cannot be loaded or resolved."""
479568

569+
_default_retryable: bool | None = False
570+
480571
def __init__(self, module_id: str, reason: str, **kwargs: Any) -> None:
481572
super().__init__(
482573
code="MODULE_LOAD_ERROR",
@@ -489,6 +580,8 @@ def __init__(self, module_id: str, reason: str, **kwargs: Any) -> None:
489580
class ModuleExecuteError(ModuleError):
490581
"""Raised when module execution fails with an unhandled error."""
491582

583+
_default_retryable: bool | None = None
584+
492585
def __init__(self, module_id: str = "", message: str = "Module execution failed", **kwargs: Any) -> None:
493586
super().__init__(
494587
code="MODULE_EXECUTE_ERROR",
@@ -501,6 +594,8 @@ def __init__(self, module_id: str = "", message: str = "Module execution failed"
501594
class InternalError(ModuleError):
502595
"""Raised for unexpected internal framework errors."""
503596

597+
_default_retryable: bool | None = True
598+
504599
def __init__(self, message: str = "Internal error", **kwargs: Any) -> None:
505600
super().__init__(
506601
code="GENERAL_INTERNAL_ERROR",

0 commit comments

Comments
 (0)