provide-foundation 0.0.0.dev0__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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,133 @@
1
+ """Resource and filesystem-related exceptions."""
2
+
3
+ from typing import Any
4
+
5
+ from provide.foundation.errors.base import FoundationError
6
+
7
+
8
+ class ResourceError(FoundationError):
9
+ """Raised when resource operations fail.
10
+
11
+ Args:
12
+ message: Error message describing the resource issue.
13
+ resource_type: Optional type of resource (file, network, etc.).
14
+ resource_path: Optional path or identifier of the resource.
15
+ **kwargs: Additional context passed to FoundationError.
16
+
17
+ Examples:
18
+ >>> raise ResourceError("File not found")
19
+ >>> raise ResourceError("Permission denied", resource_type="file", resource_path="/etc/config")
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ resource_type: str | None = None,
27
+ resource_path: str | None = None,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ if resource_type:
31
+ kwargs.setdefault("context", {})["resource.type"] = resource_type
32
+ if resource_path:
33
+ kwargs.setdefault("context", {})["resource.path"] = resource_path
34
+ super().__init__(message, **kwargs)
35
+
36
+ def _default_code(self) -> str:
37
+ return "RESOURCE_ERROR"
38
+
39
+
40
+ class NotFoundError(FoundationError):
41
+ """Raised when a requested resource cannot be found.
42
+
43
+ Args:
44
+ message: Error message describing what was not found.
45
+ resource_type: Optional type of resource.
46
+ resource_id: Optional resource identifier.
47
+ **kwargs: Additional context passed to FoundationError.
48
+
49
+ Examples:
50
+ >>> raise NotFoundError("User not found")
51
+ >>> raise NotFoundError("Entity missing", resource_type="user", resource_id="123")
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ message: str,
57
+ *,
58
+ resource_type: str | None = None,
59
+ resource_id: str | None = None,
60
+ **kwargs: Any,
61
+ ) -> None:
62
+ if resource_type:
63
+ kwargs.setdefault("context", {})["notfound.type"] = resource_type
64
+ if resource_id:
65
+ kwargs.setdefault("context", {})["notfound.id"] = resource_id
66
+ super().__init__(message, **kwargs)
67
+
68
+ def _default_code(self) -> str:
69
+ return "NOT_FOUND_ERROR"
70
+
71
+
72
+ class AlreadyExistsError(FoundationError):
73
+ """Raised when attempting to create a resource that already exists.
74
+
75
+ Args:
76
+ message: Error message describing the conflict.
77
+ resource_type: Optional type of resource.
78
+ resource_id: Optional resource identifier.
79
+ **kwargs: Additional context passed to FoundationError.
80
+
81
+ Examples:
82
+ >>> raise AlreadyExistsError("User already registered")
83
+ >>> raise AlreadyExistsError("Duplicate key", resource_type="user", resource_id="john@example.com")
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ message: str,
89
+ *,
90
+ resource_type: str | None = None,
91
+ resource_id: str | None = None,
92
+ **kwargs: Any,
93
+ ) -> None:
94
+ if resource_type:
95
+ kwargs.setdefault("context", {})["exists.type"] = resource_type
96
+ if resource_id:
97
+ kwargs.setdefault("context", {})["exists.id"] = resource_id
98
+ super().__init__(message, **kwargs)
99
+
100
+ def _default_code(self) -> str:
101
+ return "ALREADY_EXISTS_ERROR"
102
+
103
+
104
+ class LockError(FoundationError):
105
+ """Raised when file lock operations fail.
106
+
107
+ Args:
108
+ message: Error message describing the lock issue.
109
+ lock_path: Optional path to the lock file.
110
+ timeout: Optional timeout that was exceeded.
111
+ **kwargs: Additional context passed to FoundationError.
112
+
113
+ Examples:
114
+ >>> raise LockError("Failed to acquire lock")
115
+ >>> raise LockError("Lock timeout", lock_path="/tmp/app.lock", timeout=30)
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ message: str,
121
+ *,
122
+ lock_path: str | None = None,
123
+ timeout: float | None = None,
124
+ **kwargs: Any,
125
+ ) -> None:
126
+ if lock_path:
127
+ kwargs.setdefault("context", {})["lock.path"] = lock_path
128
+ if timeout is not None:
129
+ kwargs.setdefault("context", {})["lock.timeout"] = timeout
130
+ super().__init__(message, **kwargs)
131
+
132
+ def _default_code(self) -> str:
133
+ return "LOCK_ERROR"
@@ -0,0 +1,160 @@
1
+ """Runtime and process execution exceptions."""
2
+
3
+ from typing import Any
4
+
5
+ from provide.foundation.errors.base import FoundationError
6
+
7
+
8
+ class RuntimeError(FoundationError):
9
+ """Raised for runtime operational errors.
10
+
11
+ Args:
12
+ message: Error message describing the runtime issue.
13
+ operation: Optional operation that failed.
14
+ retry_possible: Whether the operation can be retried.
15
+ **kwargs: Additional context passed to FoundationError.
16
+
17
+ Examples:
18
+ >>> raise RuntimeError("Process failed")
19
+ >>> raise RuntimeError("Lock timeout", operation="acquire_lock", retry_possible=True)
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ operation: str | None = None,
27
+ retry_possible: bool = False,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ if operation:
31
+ kwargs.setdefault("context", {})["runtime.operation"] = operation
32
+ kwargs.setdefault("context", {})["runtime.retry_possible"] = retry_possible
33
+ super().__init__(message, **kwargs)
34
+
35
+ def _default_code(self) -> str:
36
+ return "RUNTIME_ERROR"
37
+
38
+
39
+ class ProcessError(RuntimeError):
40
+ """Raised when process execution fails.
41
+
42
+ Args:
43
+ message: Error message describing the process failure.
44
+ command: Optional command that was executed.
45
+ returncode: Optional process return code.
46
+ stdout: Optional captured stdout.
47
+ stderr: Optional captured stderr.
48
+ **kwargs: Additional context passed to RuntimeError.
49
+
50
+ Examples:
51
+ >>> raise ProcessError("Command failed")
52
+ >>> raise ProcessError("Build failed", command="make", returncode=2)
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ message: str,
58
+ *,
59
+ command: str | list[str] | None = None,
60
+ returncode: int | None = None,
61
+ stdout: str | None = None,
62
+ stderr: str | None = None,
63
+ **kwargs: Any,
64
+ ) -> None:
65
+ # Store as attributes for compatibility
66
+ self.command = command
67
+ self.returncode = returncode
68
+ self.stdout = stdout
69
+ self.stderr = stderr
70
+
71
+ # Also store in context for structured logging
72
+ if command:
73
+ cmd_str = " ".join(command) if isinstance(command, list) else command
74
+ kwargs.setdefault("context", {})["process.command"] = cmd_str
75
+ if returncode is not None:
76
+ kwargs.setdefault("context", {})["process.returncode"] = returncode
77
+ if stdout:
78
+ kwargs.setdefault("context", {})["process.stdout"] = stdout
79
+ if stderr:
80
+ kwargs.setdefault("context", {})["process.stderr"] = stderr
81
+ super().__init__(message, **kwargs)
82
+
83
+ def _default_code(self) -> str:
84
+ return "PROCESS_ERROR"
85
+
86
+
87
+ class StateError(FoundationError):
88
+ """Raised when an operation is invalid for the current state.
89
+
90
+ Args:
91
+ message: Error message describing the state issue.
92
+ current_state: Optional current state.
93
+ expected_state: Optional expected state.
94
+ transition: Optional attempted transition.
95
+ **kwargs: Additional context passed to FoundationError.
96
+
97
+ Examples:
98
+ >>> raise StateError("Invalid state transition")
99
+ >>> raise StateError("Not ready", current_state="initializing", expected_state="ready")
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ message: str,
105
+ *,
106
+ current_state: str | None = None,
107
+ expected_state: str | None = None,
108
+ transition: str | None = None,
109
+ **kwargs: Any,
110
+ ) -> None:
111
+ if current_state:
112
+ kwargs.setdefault("context", {})["state.current"] = current_state
113
+ if expected_state:
114
+ kwargs.setdefault("context", {})["state.expected"] = expected_state
115
+ if transition:
116
+ kwargs.setdefault("context", {})["state.transition"] = transition
117
+ super().__init__(message, **kwargs)
118
+
119
+ def _default_code(self) -> str:
120
+ return "STATE_ERROR"
121
+
122
+
123
+ class ConcurrencyError(FoundationError):
124
+ """Raised when concurrency conflicts occur.
125
+
126
+ Args:
127
+ message: Error message describing the concurrency issue.
128
+ conflict_type: Optional type of conflict (lock, version, etc.).
129
+ version_expected: Optional expected version.
130
+ version_actual: Optional actual version.
131
+ **kwargs: Additional context passed to FoundationError.
132
+
133
+ Examples:
134
+ >>> raise ConcurrencyError("Optimistic lock failure")
135
+ >>> raise ConcurrencyError("Version mismatch", version_expected=1, version_actual=2)
136
+ """
137
+
138
+ def __init__(
139
+ self,
140
+ message: str,
141
+ *,
142
+ conflict_type: str | None = None,
143
+ version_expected: Any = None,
144
+ version_actual: Any = None,
145
+ **kwargs: Any,
146
+ ) -> None:
147
+ if conflict_type:
148
+ kwargs.setdefault("context", {})["concurrency.type"] = conflict_type
149
+ if version_expected is not None:
150
+ kwargs.setdefault("context", {})["concurrency.version_expected"] = str(
151
+ version_expected
152
+ )
153
+ if version_actual is not None:
154
+ kwargs.setdefault("context", {})["concurrency.version_actual"] = str(
155
+ version_actual
156
+ )
157
+ super().__init__(message, **kwargs)
158
+
159
+ def _default_code(self) -> str:
160
+ return "CONCURRENCY_ERROR"
@@ -0,0 +1,133 @@
1
+ """Safe error decorators that preserve original behavior."""
2
+
3
+ from collections.abc import Callable
4
+ import functools
5
+ import inspect
6
+ from typing import Any, TypeVar
7
+
8
+ F = TypeVar("F", bound=Callable[..., Any])
9
+
10
+
11
+ def _get_logger():
12
+ """Get logger instance lazily to avoid circular imports."""
13
+ from provide.foundation.logger import logger
14
+
15
+ return logger
16
+
17
+
18
+ def log_only_error_context(
19
+ *,
20
+ context_provider: Callable[[], dict[str, Any]] | None = None,
21
+ log_level: str = "debug",
22
+ log_success: bool = False,
23
+ ) -> Callable[[F], F]:
24
+ """Safe decorator that only adds logging context without changing error behavior.
25
+
26
+ This decorator preserves the exact original error message and type while adding
27
+ structured logging context. It never suppresses errors or changes their behavior.
28
+
29
+ Args:
30
+ context_provider: Function that provides additional logging context.
31
+ log_level: Level for operation logging ('debug', 'trace', etc.)
32
+ log_success: Whether to log successful operations.
33
+
34
+ Returns:
35
+ Decorated function that preserves all original error behavior.
36
+
37
+ Examples:
38
+ >>> @log_only_error_context(
39
+ ... context_provider=lambda: {"operation": "detect_launcher_type"},
40
+ ... log_level="trace"
41
+ ... )
42
+ ... def detect_launcher_type(self, path):
43
+ ... # Original error messages preserved exactly
44
+ ... return self._internal_detect(path)
45
+ """
46
+
47
+ def decorator(func: F) -> F:
48
+ if inspect.iscoroutinefunction(func):
49
+
50
+ @functools.wraps(func)
51
+ async def async_wrapper(*args, **kwargs):
52
+ context = context_provider() if context_provider else {}
53
+ logger = _get_logger()
54
+
55
+ # Log function entry if debug/trace level
56
+ if log_level in ("debug", "trace"):
57
+ log_method = getattr(logger, log_level)
58
+ log_method(
59
+ f"Entering {func.__name__}", function=func.__name__, **context
60
+ )
61
+
62
+ try:
63
+ result = await func(*args, **kwargs)
64
+
65
+ # Log success if requested
66
+ if log_success:
67
+ log_method = getattr(logger, log_level, logger.debug)
68
+ log_method(
69
+ f"Successfully completed {func.__name__}",
70
+ function=func.__name__,
71
+ **context,
72
+ )
73
+
74
+ return result
75
+
76
+ except Exception as e:
77
+ # Log error context without changing the error
78
+ logger.error(
79
+ f"Error in {func.__name__}",
80
+ exc_info=True,
81
+ function=func.__name__,
82
+ error_type=type(e).__name__,
83
+ error_message=str(e),
84
+ **context,
85
+ )
86
+ # Re-raise the original error unchanged
87
+ raise
88
+
89
+ return async_wrapper # type: ignore
90
+ else:
91
+
92
+ @functools.wraps(func)
93
+ def wrapper(*args, **kwargs):
94
+ context = context_provider() if context_provider else {}
95
+ logger = _get_logger()
96
+
97
+ # Log function entry if debug/trace level
98
+ if log_level in ("debug", "trace"):
99
+ log_method = getattr(logger, log_level)
100
+ log_method(
101
+ f"Entering {func.__name__}", function=func.__name__, **context
102
+ )
103
+
104
+ try:
105
+ result = func(*args, **kwargs)
106
+
107
+ # Log success if requested
108
+ if log_success:
109
+ log_method = getattr(logger, log_level, logger.debug)
110
+ log_method(
111
+ f"Successfully completed {func.__name__}",
112
+ function=func.__name__,
113
+ **context,
114
+ )
115
+
116
+ return result
117
+
118
+ except Exception as e:
119
+ # Log error context without changing the error
120
+ logger.error(
121
+ f"Error in {func.__name__}",
122
+ exc_info=True,
123
+ function=func.__name__,
124
+ error_type=type(e).__name__,
125
+ error_message=str(e),
126
+ **context,
127
+ )
128
+ # Re-raise the original error unchanged
129
+ raise
130
+
131
+ return wrapper # type: ignore
132
+
133
+ return decorator
@@ -0,0 +1,276 @@
1
+ """Type definitions and constants for error handling.
2
+
3
+ Provides error codes, metadata structures, and retry policies.
4
+ """
5
+
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from attrs import define, field
10
+
11
+
12
+ class ErrorCode(str, Enum):
13
+ """Standard error codes for programmatic error handling.
14
+
15
+ Use these codes for consistent error identification across the system.
16
+ Codes are grouped by category with prefixes:
17
+ - CFG: Configuration errors
18
+ - VAL: Validation errors
19
+ - INT: Integration errors
20
+ - RES: Resource errors
21
+ - AUTH: Authentication/Authorization errors
22
+ - SYS: System errors
23
+ """
24
+
25
+ # Configuration errors (CFG)
26
+ CONFIG_INVALID = "CFG_001"
27
+ CONFIG_MISSING = "CFG_002"
28
+ CONFIG_PARSE_ERROR = "CFG_003"
29
+ CONFIG_TYPE_ERROR = "CFG_004"
30
+
31
+ # Validation errors (VAL)
32
+ VALIDATION_TYPE = "VAL_001"
33
+ VALIDATION_RANGE = "VAL_002"
34
+ VALIDATION_FORMAT = "VAL_003"
35
+ VALIDATION_REQUIRED = "VAL_004"
36
+ VALIDATION_CONSTRAINT = "VAL_005"
37
+
38
+ # Integration errors (INT)
39
+ INTEGRATION_TIMEOUT = "INT_001"
40
+ INTEGRATION_AUTH = "INT_002"
41
+ INTEGRATION_UNAVAILABLE = "INT_003"
42
+ INTEGRATION_RATE_LIMIT = "INT_004"
43
+ INTEGRATION_PROTOCOL = "INT_005"
44
+
45
+ # Resource errors (RES)
46
+ RESOURCE_NOT_FOUND = "RES_001"
47
+ RESOURCE_LOCKED = "RES_002"
48
+ RESOURCE_PERMISSION = "RES_003"
49
+ RESOURCE_EXHAUSTED = "RES_004"
50
+ RESOURCE_CONFLICT = "RES_005"
51
+
52
+ # Authentication/Authorization errors (AUTH)
53
+ AUTH_INVALID_CREDENTIALS = "AUTH_001"
54
+ AUTH_TOKEN_EXPIRED = "AUTH_002"
55
+ AUTH_INSUFFICIENT_PERMISSION = "AUTH_003"
56
+ AUTH_SESSION_INVALID = "AUTH_004"
57
+ AUTH_MFA_REQUIRED = "AUTH_005"
58
+
59
+ # System errors (SYS)
60
+ SYSTEM_UNAVAILABLE = "SYS_001"
61
+ SYSTEM_OVERLOAD = "SYS_002"
62
+ SYSTEM_MAINTENANCE = "SYS_003"
63
+ SYSTEM_INTERNAL = "SYS_004"
64
+ SYSTEM_PANIC = "SYS_005"
65
+
66
+
67
+ @define(kw_only=True, slots=True)
68
+ class ErrorMetadata:
69
+ """Additional metadata for error tracking and debugging.
70
+
71
+ Attributes:
72
+ request_id: Optional request identifier for tracing.
73
+ user_id: Optional user identifier.
74
+ session_id: Optional session identifier.
75
+ correlation_id: Optional correlation ID for distributed tracing.
76
+ retry_count: Number of retry attempts made.
77
+ retry_after: Seconds to wait before retry.
78
+ help_url: Optional URL for more information.
79
+ support_id: Optional support ticket/case ID.
80
+
81
+ Examples:
82
+ >>> meta = ErrorMetadata(
83
+ ... request_id="req_123",
84
+ ... user_id="user_456",
85
+ ... retry_count=2
86
+ ... )
87
+ """
88
+
89
+ request_id: str | None = None
90
+ user_id: str | None = None
91
+ session_id: str | None = None
92
+ correlation_id: str | None = None
93
+ retry_count: int = 0
94
+ retry_after: float | None = None
95
+ help_url: str | None = None
96
+ support_id: str | None = None
97
+
98
+ def to_dict(self) -> dict[str, Any]:
99
+ """Convert to dictionary, excluding None values.
100
+
101
+ Returns:
102
+ Dictionary with non-None metadata fields.
103
+ """
104
+ result = {}
105
+ for key in [
106
+ "request_id",
107
+ "user_id",
108
+ "session_id",
109
+ "correlation_id",
110
+ "retry_count",
111
+ "retry_after",
112
+ "help_url",
113
+ "support_id",
114
+ ]:
115
+ value = getattr(self, key)
116
+ if value is not None:
117
+ result[key] = value
118
+ return result
119
+
120
+
121
+ class BackoffStrategy(str, Enum):
122
+ """Backoff strategies for retry policies."""
123
+
124
+ FIXED = "fixed" # Fixed delay between retries
125
+ LINEAR = "linear" # Linear increase (delay * attempt)
126
+ EXPONENTIAL = "exponential" # Exponential increase (delay * 2^attempt)
127
+ FIBONACCI = "fibonacci" # Fibonacci sequence delays
128
+
129
+
130
+ @define(kw_only=True, slots=True)
131
+ class RetryPolicy:
132
+ """Configuration for retry behavior on errors.
133
+
134
+ Attributes:
135
+ max_attempts: Maximum number of retry attempts.
136
+ backoff: Backoff strategy to use.
137
+ base_delay: Base delay in seconds between retries.
138
+ max_delay: Maximum delay in seconds (caps exponential growth).
139
+ jitter: Whether to add random jitter to delays.
140
+ retryable_errors: Optional tuple of exception types to retry on.
141
+
142
+ Examples:
143
+ >>> policy = RetryPolicy(
144
+ ... max_attempts=5,
145
+ ... backoff=BackoffStrategy.EXPONENTIAL,
146
+ ... base_delay=1.0,
147
+ ... max_delay=30.0
148
+ ... )
149
+ """
150
+
151
+ max_attempts: int = 3
152
+ backoff: BackoffStrategy = BackoffStrategy.EXPONENTIAL
153
+ base_delay: float = 1.0
154
+ max_delay: float = 60.0
155
+ jitter: bool = True
156
+ retryable_errors: tuple[type[Exception], ...] | None = None
157
+
158
+ def calculate_delay(self, attempt: int) -> float:
159
+ """Calculate delay for a given attempt number.
160
+
161
+ Args:
162
+ attempt: Attempt number (1-based).
163
+
164
+ Returns:
165
+ Delay in seconds.
166
+ """
167
+ if attempt <= 0:
168
+ return 0
169
+
170
+ if self.backoff == BackoffStrategy.FIXED:
171
+ delay = self.base_delay
172
+ elif self.backoff == BackoffStrategy.LINEAR:
173
+ delay = self.base_delay * attempt
174
+ elif self.backoff == BackoffStrategy.EXPONENTIAL:
175
+ delay = self.base_delay * (2 ** (attempt - 1))
176
+ elif self.backoff == BackoffStrategy.FIBONACCI:
177
+ # Calculate fibonacci number for attempt
178
+ a, b = 0, 1
179
+ for _ in range(attempt):
180
+ a, b = b, a + b
181
+ delay = self.base_delay * a
182
+ else:
183
+ delay = self.base_delay
184
+
185
+ # Cap at max delay
186
+ delay = min(delay, self.max_delay)
187
+
188
+ # Add jitter if configured (±25% random variation)
189
+ if self.jitter:
190
+ import random
191
+
192
+ jitter_factor = 0.75 + (random.random() * 0.5)
193
+ delay *= jitter_factor
194
+
195
+ return delay
196
+
197
+ def should_retry(self, error: Exception, attempt: int) -> bool:
198
+ """Determine if an error should be retried.
199
+
200
+ Args:
201
+ error: The exception that occurred.
202
+ attempt: Current attempt number (1-based).
203
+
204
+ Returns:
205
+ True if should retry, False otherwise.
206
+ """
207
+ # Check attempt limit
208
+ if attempt >= self.max_attempts:
209
+ return False
210
+
211
+ # Check error type if filter is configured
212
+ if self.retryable_errors is not None:
213
+ return isinstance(error, self.retryable_errors)
214
+
215
+ # Default to retry for any error
216
+ return True
217
+
218
+
219
+ @define(kw_only=True, slots=True)
220
+ class ErrorResponse:
221
+ """Structured error response for APIs and external interfaces.
222
+
223
+ Attributes:
224
+ error_code: Machine-readable error code.
225
+ message: Human-readable error message.
226
+ details: Optional additional error details.
227
+ metadata: Optional error metadata.
228
+ timestamp: When the error occurred.
229
+
230
+ Examples:
231
+ >>> response = ErrorResponse(
232
+ ... error_code="VAL_001",
233
+ ... message="Invalid email format",
234
+ ... details={"field": "email", "value": "not-an-email"}
235
+ ... )
236
+ """
237
+
238
+ error_code: str
239
+ message: str
240
+ details: dict[str, Any] | None = None
241
+ metadata: ErrorMetadata | None = None
242
+ timestamp: str = field(factory=lambda: datetime.now().isoformat())
243
+
244
+ def to_dict(self) -> dict[str, Any]:
245
+ """Convert to dictionary for JSON serialization.
246
+
247
+ Returns:
248
+ Dictionary representation of error response.
249
+ """
250
+ result: dict[str, Any] = {
251
+ "error_code": self.error_code,
252
+ "message": self.message,
253
+ "timestamp": self.timestamp,
254
+ }
255
+
256
+ if self.details:
257
+ result["details"] = self.details
258
+
259
+ if self.metadata:
260
+ result["metadata"] = self.metadata.to_dict()
261
+
262
+ return result
263
+
264
+ def to_json(self) -> str:
265
+ """Convert to JSON string.
266
+
267
+ Returns:
268
+ JSON representation of error response.
269
+ """
270
+ import json
271
+
272
+ return json.dumps(self.to_dict(), indent=2)
273
+
274
+
275
+ # Import datetime at module level for the factory
276
+ from datetime import datetime