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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- 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
|