provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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/foundation/__init__.py +36 -10
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +93 -96
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +39 -25
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +69 -74
- provide/foundation/tools/downloader.py +68 -62
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +2 -14
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,218 @@
|
|
1
|
+
"""Core environment variable utilities for Foundation."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
|
5
|
+
from provide.foundation.errors import ValidationError
|
6
|
+
|
7
|
+
|
8
|
+
def get_env(key: str, default: str | None = None) -> str | None:
|
9
|
+
"""
|
10
|
+
Get environment variable with Foundation tracking.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
key: Environment variable name
|
14
|
+
default: Default value if variable not found
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
Environment variable value or default
|
18
|
+
|
19
|
+
Example:
|
20
|
+
>>> get_env("HOME") # doctest: +SKIP
|
21
|
+
'/Users/username'
|
22
|
+
>>> get_env("NONEXISTENT", "fallback")
|
23
|
+
'fallback'
|
24
|
+
"""
|
25
|
+
return os.environ.get(key, default)
|
26
|
+
|
27
|
+
|
28
|
+
def set_env(key: str, value: str) -> None:
|
29
|
+
"""
|
30
|
+
Set environment variable with validation.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
key: Environment variable name
|
34
|
+
value: Value to set
|
35
|
+
|
36
|
+
Raises:
|
37
|
+
ValidationError: If key or value is invalid
|
38
|
+
|
39
|
+
Example:
|
40
|
+
>>> set_env("TEST_VAR", "test_value")
|
41
|
+
>>> get_env("TEST_VAR")
|
42
|
+
'test_value'
|
43
|
+
"""
|
44
|
+
if not isinstance(key, str) or not key:
|
45
|
+
raise ValidationError("Environment variable key must be a non-empty string")
|
46
|
+
if not isinstance(value, str):
|
47
|
+
raise ValidationError("Environment variable value must be a string")
|
48
|
+
|
49
|
+
os.environ[key] = value
|
50
|
+
|
51
|
+
|
52
|
+
def unset_env(key: str) -> None:
|
53
|
+
"""
|
54
|
+
Remove environment variable if it exists.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
key: Environment variable name to remove
|
58
|
+
|
59
|
+
Example:
|
60
|
+
>>> set_env("TEMP_VAR", "value")
|
61
|
+
>>> unset_env("TEMP_VAR")
|
62
|
+
>>> has_env("TEMP_VAR")
|
63
|
+
False
|
64
|
+
"""
|
65
|
+
os.environ.pop(key, None)
|
66
|
+
|
67
|
+
|
68
|
+
def has_env(key: str) -> bool:
|
69
|
+
"""
|
70
|
+
Check if environment variable exists.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
key: Environment variable name
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
True if variable exists, False otherwise
|
77
|
+
|
78
|
+
Example:
|
79
|
+
>>> has_env("PATH")
|
80
|
+
True
|
81
|
+
>>> has_env("DEFINITELY_NOT_SET")
|
82
|
+
False
|
83
|
+
"""
|
84
|
+
return key in os.environ
|
85
|
+
|
86
|
+
|
87
|
+
def get_env_int(key: str, default: int | None = None) -> int | None:
|
88
|
+
"""
|
89
|
+
Get environment variable as integer.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
key: Environment variable name
|
93
|
+
default: Default value if variable not found or invalid
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Integer value or default
|
97
|
+
|
98
|
+
Raises:
|
99
|
+
ValidationError: If value exists but cannot be converted to int
|
100
|
+
|
101
|
+
Example:
|
102
|
+
>>> set_env("PORT", "8080")
|
103
|
+
>>> get_env_int("PORT")
|
104
|
+
8080
|
105
|
+
>>> get_env_int("MISSING_PORT", 3000)
|
106
|
+
3000
|
107
|
+
"""
|
108
|
+
value = os.environ.get(key)
|
109
|
+
if value is None:
|
110
|
+
return default
|
111
|
+
|
112
|
+
try:
|
113
|
+
return int(value)
|
114
|
+
except ValueError as e:
|
115
|
+
raise ValidationError(
|
116
|
+
f"Environment variable {key}='{value}' cannot be converted to int"
|
117
|
+
) from e
|
118
|
+
|
119
|
+
|
120
|
+
def get_env_bool(key: str, default: bool | None = None) -> bool | None:
|
121
|
+
"""
|
122
|
+
Get environment variable as boolean.
|
123
|
+
|
124
|
+
Recognizes: true/false, yes/no, 1/0, on/off (case insensitive)
|
125
|
+
|
126
|
+
Args:
|
127
|
+
key: Environment variable name
|
128
|
+
default: Default value if variable not found
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Boolean value or default
|
132
|
+
|
133
|
+
Raises:
|
134
|
+
ValidationError: If value exists but cannot be converted to bool
|
135
|
+
|
136
|
+
Example:
|
137
|
+
>>> set_env("DEBUG", "true")
|
138
|
+
>>> get_env_bool("DEBUG")
|
139
|
+
True
|
140
|
+
>>> set_env("VERBOSE", "no")
|
141
|
+
>>> get_env_bool("VERBOSE")
|
142
|
+
False
|
143
|
+
"""
|
144
|
+
value = os.environ.get(key)
|
145
|
+
if value is None:
|
146
|
+
return default
|
147
|
+
|
148
|
+
value_lower = value.lower().strip()
|
149
|
+
if value_lower in ("true", "yes", "1", "on"):
|
150
|
+
return True
|
151
|
+
elif value_lower in ("false", "no", "0", "off"):
|
152
|
+
return False
|
153
|
+
else:
|
154
|
+
raise ValidationError(
|
155
|
+
f"Environment variable {key}='{value}' cannot be converted to bool"
|
156
|
+
)
|
157
|
+
|
158
|
+
|
159
|
+
def get_env_float(key: str, default: float | None = None) -> float | None:
|
160
|
+
"""
|
161
|
+
Get environment variable as float.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
key: Environment variable name
|
165
|
+
default: Default value if variable not found or invalid
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
Float value or default
|
169
|
+
|
170
|
+
Raises:
|
171
|
+
ValidationError: If value exists but cannot be converted to float
|
172
|
+
|
173
|
+
Example:
|
174
|
+
>>> set_env("TIMEOUT", "30.5")
|
175
|
+
>>> get_env_float("TIMEOUT")
|
176
|
+
30.5
|
177
|
+
"""
|
178
|
+
value = os.environ.get(key)
|
179
|
+
if value is None:
|
180
|
+
return default
|
181
|
+
|
182
|
+
try:
|
183
|
+
return float(value)
|
184
|
+
except ValueError as e:
|
185
|
+
raise ValidationError(
|
186
|
+
f"Environment variable {key}='{value}' cannot be converted to float"
|
187
|
+
) from e
|
188
|
+
|
189
|
+
|
190
|
+
def get_env_list(
|
191
|
+
key: str, separator: str = ",", default: list[str] | None = None
|
192
|
+
) -> list[str] | None:
|
193
|
+
"""
|
194
|
+
Get environment variable as list of strings.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
key: Environment variable name
|
198
|
+
separator: Character to split on (default: comma)
|
199
|
+
default: Default value if variable not found
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
List of strings or default
|
203
|
+
|
204
|
+
Example:
|
205
|
+
>>> set_env("ALLOWED_HOSTS", "localhost,127.0.0.1,example.com")
|
206
|
+
>>> get_env_list("ALLOWED_HOSTS")
|
207
|
+
['localhost', '127.0.0.1', 'example.com']
|
208
|
+
>>> get_env_list("MISSING", default=["fallback"])
|
209
|
+
['fallback']
|
210
|
+
"""
|
211
|
+
value = os.environ.get(key)
|
212
|
+
if value is None:
|
213
|
+
return default
|
214
|
+
|
215
|
+
if not value.strip():
|
216
|
+
return []
|
217
|
+
|
218
|
+
return [item.strip() for item in value.split(separator) if item.strip()]
|
@@ -20,7 +20,6 @@ from provide.foundation.errors.context import (
|
|
20
20
|
)
|
21
21
|
from provide.foundation.errors.decorators import (
|
22
22
|
fallback_on_error,
|
23
|
-
retry_on_error,
|
24
23
|
suppress_and_log,
|
25
24
|
with_error_handling,
|
26
25
|
)
|
@@ -50,9 +49,11 @@ from provide.foundation.errors.safe_decorators import log_only_error_context
|
|
50
49
|
from provide.foundation.errors.types import (
|
51
50
|
ErrorCode,
|
52
51
|
ErrorMetadata,
|
53
|
-
RetryPolicy,
|
54
52
|
)
|
55
53
|
|
54
|
+
# Re-export from resilience module for compatibility
|
55
|
+
from provide.foundation.resilience.decorators import retry as retry_on_error
|
56
|
+
|
56
57
|
__all__ = [
|
57
58
|
"AlreadyExistsError",
|
58
59
|
"AuthenticationError",
|
@@ -77,7 +78,6 @@ __all__ = [
|
|
77
78
|
"ProcessError",
|
78
79
|
"ProcessTimeoutError",
|
79
80
|
"ResourceError",
|
80
|
-
"RetryPolicy",
|
81
81
|
"RuntimeError",
|
82
82
|
"StateError",
|
83
83
|
"TimeoutError",
|
@@ -7,13 +7,9 @@ fallback, and error suppression.
|
|
7
7
|
from collections.abc import Callable
|
8
8
|
import functools
|
9
9
|
import inspect
|
10
|
-
import time
|
11
10
|
from typing import Any, TypeVar
|
12
11
|
|
13
|
-
from attrs import define, field
|
14
|
-
|
15
12
|
from provide.foundation.errors.base import FoundationError
|
16
|
-
from provide.foundation.errors.types import RetryPolicy
|
17
13
|
|
18
14
|
F = TypeVar("F", bound=Callable[..., Any])
|
19
15
|
|
@@ -148,126 +144,6 @@ def with_error_handling(
|
|
148
144
|
return decorator(func)
|
149
145
|
|
150
146
|
|
151
|
-
def retry_on_error(
|
152
|
-
*exceptions: type[Exception],
|
153
|
-
policy: RetryPolicy | None = None,
|
154
|
-
max_attempts: int | None = None,
|
155
|
-
delay: float | None = None,
|
156
|
-
backoff: float | None = None,
|
157
|
-
on_retry: Callable[[int, Exception], None] | None = None,
|
158
|
-
) -> Callable[[F], F]:
|
159
|
-
"""Decorator for retrying operations on specific errors.
|
160
|
-
|
161
|
-
Args:
|
162
|
-
*exceptions: Exception types to retry on (all if empty).
|
163
|
-
policy: Complete retry policy (overrides other retry params).
|
164
|
-
max_attempts: Maximum retry attempts (ignored if policy provided).
|
165
|
-
delay: Base delay between retries in seconds.
|
166
|
-
backoff: Backoff multiplier for delays.
|
167
|
-
on_retry: Callback function called before each retry.
|
168
|
-
|
169
|
-
Returns:
|
170
|
-
Decorated function.
|
171
|
-
|
172
|
-
Examples:
|
173
|
-
>>> @retry_on_error(ConnectionError, TimeoutError, max_attempts=3)
|
174
|
-
... def fetch_data():
|
175
|
-
... return api_call()
|
176
|
-
|
177
|
-
>>> @retry_on_error(
|
178
|
-
... policy=RetryPolicy(max_attempts=5, backoff="exponential")
|
179
|
-
... )
|
180
|
-
... def unreliable_operation():
|
181
|
-
... pass
|
182
|
-
"""
|
183
|
-
# Use provided policy or create one from parameters
|
184
|
-
if policy is None:
|
185
|
-
from provide.foundation.errors.types import BackoffStrategy
|
186
|
-
|
187
|
-
# Determine backoff strategy
|
188
|
-
if backoff is not None and backoff > 1:
|
189
|
-
backoff_strategy = BackoffStrategy.EXPONENTIAL
|
190
|
-
elif backoff == 1:
|
191
|
-
backoff_strategy = BackoffStrategy.FIXED
|
192
|
-
else:
|
193
|
-
backoff_strategy = BackoffStrategy.EXPONENTIAL
|
194
|
-
|
195
|
-
policy = RetryPolicy(
|
196
|
-
max_attempts=max_attempts or 3,
|
197
|
-
base_delay=delay or 1.0,
|
198
|
-
backoff=backoff_strategy,
|
199
|
-
retryable_errors=exceptions if exceptions else None,
|
200
|
-
)
|
201
|
-
|
202
|
-
def decorator(func: F) -> F:
|
203
|
-
@functools.wraps(func)
|
204
|
-
def wrapper(*args, **kwargs):
|
205
|
-
last_exception = None
|
206
|
-
|
207
|
-
for attempt in range(1, policy.max_attempts + 1):
|
208
|
-
try:
|
209
|
-
return func(*args, **kwargs)
|
210
|
-
except Exception as e:
|
211
|
-
last_exception = e
|
212
|
-
|
213
|
-
# Check if we should retry this error
|
214
|
-
if not policy.should_retry(e, attempt):
|
215
|
-
if attempt > 1: # Only log if we've actually retried
|
216
|
-
_get_logger().error(
|
217
|
-
f"All {attempt} retry attempts failed for {func.__name__}",
|
218
|
-
attempts=attempt,
|
219
|
-
error=str(e),
|
220
|
-
error_type=type(e).__name__,
|
221
|
-
)
|
222
|
-
raise
|
223
|
-
|
224
|
-
# Don't retry on last attempt
|
225
|
-
if attempt >= policy.max_attempts:
|
226
|
-
_get_logger().error(
|
227
|
-
f"All {policy.max_attempts} retry attempts failed for {func.__name__}",
|
228
|
-
attempts=policy.max_attempts,
|
229
|
-
error=str(e),
|
230
|
-
error_type=type(e).__name__,
|
231
|
-
)
|
232
|
-
raise
|
233
|
-
|
234
|
-
# Calculate delay
|
235
|
-
retry_delay = policy.calculate_delay(attempt)
|
236
|
-
|
237
|
-
# Log retry attempt
|
238
|
-
_get_logger().warning(
|
239
|
-
f"Retry {attempt}/{policy.max_attempts} for {func.__name__} after {retry_delay:.2f}s",
|
240
|
-
function=func.__name__,
|
241
|
-
attempt=attempt,
|
242
|
-
max_attempts=policy.max_attempts,
|
243
|
-
delay=retry_delay,
|
244
|
-
error=str(e),
|
245
|
-
error_type=type(e).__name__,
|
246
|
-
)
|
247
|
-
|
248
|
-
# Call retry callback if provided
|
249
|
-
if on_retry:
|
250
|
-
try:
|
251
|
-
on_retry(attempt, e)
|
252
|
-
except Exception as callback_error:
|
253
|
-
_get_logger().warning(
|
254
|
-
f"Retry callback failed: {callback_error}",
|
255
|
-
function=func.__name__,
|
256
|
-
attempt=attempt,
|
257
|
-
)
|
258
|
-
|
259
|
-
# Wait before retry
|
260
|
-
time.sleep(retry_delay)
|
261
|
-
|
262
|
-
# Should never reach here, but just in case
|
263
|
-
if last_exception:
|
264
|
-
raise last_exception
|
265
|
-
|
266
|
-
return wrapper # type: ignore
|
267
|
-
|
268
|
-
return decorator
|
269
|
-
|
270
|
-
|
271
147
|
def suppress_and_log(
|
272
148
|
*exceptions: type[Exception],
|
273
149
|
fallback: Any = None,
|
@@ -372,113 +248,3 @@ def fallback_on_error(
|
|
372
248
|
return wrapper # type: ignore
|
373
249
|
|
374
250
|
return decorator
|
375
|
-
|
376
|
-
|
377
|
-
@define(kw_only=True, slots=True)
|
378
|
-
class CircuitBreaker:
|
379
|
-
"""Circuit breaker pattern for preventing cascading failures.
|
380
|
-
|
381
|
-
Attributes:
|
382
|
-
failure_threshold: Number of failures before opening circuit.
|
383
|
-
recovery_timeout: Seconds to wait before attempting recovery.
|
384
|
-
expected_exception: Exception types that trigger the breaker.
|
385
|
-
"""
|
386
|
-
|
387
|
-
failure_threshold: int = 5
|
388
|
-
recovery_timeout: float = 60.0
|
389
|
-
expected_exception: tuple[type[Exception], ...] = field(default=(Exception,))
|
390
|
-
|
391
|
-
# Internal state
|
392
|
-
_failure_count: int = field(init=False, default=0)
|
393
|
-
_last_failure_time: float | None = field(init=False, default=None)
|
394
|
-
_state: str = field(init=False, default="closed") # closed, open, half_open
|
395
|
-
|
396
|
-
def __call__(self, func: F) -> F:
|
397
|
-
"""Decorator to apply circuit breaker to a function."""
|
398
|
-
|
399
|
-
@functools.wraps(func)
|
400
|
-
def wrapper(*args, **kwargs):
|
401
|
-
# Check circuit state
|
402
|
-
if self._state == "open":
|
403
|
-
# Check if we should try half-open
|
404
|
-
if (
|
405
|
-
self._last_failure_time
|
406
|
-
and (time.time() - self._last_failure_time) > self.recovery_timeout
|
407
|
-
):
|
408
|
-
self._state = "half_open"
|
409
|
-
_get_logger().info(
|
410
|
-
f"Circuit breaker for {func.__name__} entering half-open state",
|
411
|
-
function=func.__name__,
|
412
|
-
)
|
413
|
-
else:
|
414
|
-
raise RuntimeError(f"Circuit breaker is open for {func.__name__}")
|
415
|
-
|
416
|
-
try:
|
417
|
-
result = func(*args, **kwargs)
|
418
|
-
|
419
|
-
# Success - reset on half-open or reduce failure count
|
420
|
-
if self._state == "half_open":
|
421
|
-
self._state = "closed"
|
422
|
-
self._failure_count = 0
|
423
|
-
_get_logger().info(
|
424
|
-
f"Circuit breaker for {func.__name__} closed after successful recovery",
|
425
|
-
function=func.__name__,
|
426
|
-
)
|
427
|
-
elif self._failure_count > 0:
|
428
|
-
self._failure_count = max(0, self._failure_count - 1)
|
429
|
-
|
430
|
-
return result
|
431
|
-
|
432
|
-
except self.expected_exception as e:
|
433
|
-
self._failure_count += 1
|
434
|
-
self._last_failure_time = time.time()
|
435
|
-
|
436
|
-
# Check if we should open the circuit
|
437
|
-
if self._failure_count >= self.failure_threshold:
|
438
|
-
self._state = "open"
|
439
|
-
_get_logger().error(
|
440
|
-
f"Circuit breaker for {func.__name__} opened after {self._failure_count} failures",
|
441
|
-
function=func.__name__,
|
442
|
-
failures=self._failure_count,
|
443
|
-
error=str(e),
|
444
|
-
)
|
445
|
-
else:
|
446
|
-
_get_logger().warning(
|
447
|
-
f"Circuit breaker for {func.__name__} failure {self._failure_count}/{self.failure_threshold}",
|
448
|
-
function=func.__name__,
|
449
|
-
failures=self._failure_count,
|
450
|
-
threshold=self.failure_threshold,
|
451
|
-
error=str(e),
|
452
|
-
)
|
453
|
-
|
454
|
-
raise
|
455
|
-
|
456
|
-
return wrapper # type: ignore
|
457
|
-
|
458
|
-
|
459
|
-
def circuit_breaker(
|
460
|
-
failure_threshold: int = 5,
|
461
|
-
recovery_timeout: float = 60.0,
|
462
|
-
expected_exception: tuple[type[Exception], ...] = (Exception,),
|
463
|
-
) -> Callable[[F], F]:
|
464
|
-
"""Create a circuit breaker decorator.
|
465
|
-
|
466
|
-
Args:
|
467
|
-
failure_threshold: Number of failures before opening circuit.
|
468
|
-
recovery_timeout: Seconds to wait before attempting recovery.
|
469
|
-
expected_exception: Exception types that trigger the breaker.
|
470
|
-
|
471
|
-
Returns:
|
472
|
-
Circuit breaker decorator.
|
473
|
-
|
474
|
-
Examples:
|
475
|
-
>>> @circuit_breaker(failure_threshold=3, recovery_timeout=30)
|
476
|
-
... def unreliable_service():
|
477
|
-
... return external_api_call()
|
478
|
-
"""
|
479
|
-
breaker = CircuitBreaker(
|
480
|
-
failure_threshold=failure_threshold,
|
481
|
-
recovery_timeout=recovery_timeout,
|
482
|
-
expected_exception=expected_exception,
|
483
|
-
)
|
484
|
-
return breaker
|
@@ -118,104 +118,6 @@ class ErrorMetadata:
|
|
118
118
|
return result
|
119
119
|
|
120
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
121
|
@define(kw_only=True, slots=True)
|
220
122
|
class ErrorResponse:
|
221
123
|
"""Structured error response for APIs and external interfaces.
|
@@ -2,10 +2,9 @@
|
|
2
2
|
Event set display utilities for Foundation.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from provide.foundation.
|
6
|
-
|
7
|
-
from provide.foundation.eventsets.registry import get_registry, discover_event_sets
|
5
|
+
from provide.foundation.eventsets.registry import discover_event_sets, get_registry
|
8
6
|
from provide.foundation.eventsets.resolver import get_resolver
|
7
|
+
from provide.foundation.logger import get_logger
|
9
8
|
|
10
9
|
logger = get_logger(__name__)
|
11
10
|
|
@@ -17,16 +16,16 @@ def show_event_matrix() -> None:
|
|
17
16
|
"""
|
18
17
|
# Ensure event sets are discovered
|
19
18
|
discover_event_sets()
|
20
|
-
|
19
|
+
|
21
20
|
registry = get_registry()
|
22
21
|
resolver = get_resolver()
|
23
|
-
|
22
|
+
|
24
23
|
# Force resolution to ensure everything is loaded
|
25
24
|
resolver.resolve()
|
26
|
-
|
25
|
+
|
27
26
|
lines: list[str] = ["Foundation Event Sets: Active Configuration"]
|
28
27
|
lines.append("=" * 70)
|
29
|
-
|
28
|
+
|
30
29
|
# Show registered event sets
|
31
30
|
event_sets = registry.list_event_sets()
|
32
31
|
if event_sets:
|
@@ -35,7 +34,7 @@ def show_event_matrix() -> None:
|
|
35
34
|
lines.append(f"\n {config.name} (priority: {config.priority})")
|
36
35
|
if config.description:
|
37
36
|
lines.append(f" {config.description}")
|
38
|
-
|
37
|
+
|
39
38
|
# Show field mappings
|
40
39
|
if config.field_mappings:
|
41
40
|
lines.append(f" Field Mappings ({len(config.field_mappings)}):")
|
@@ -43,7 +42,7 @@ def show_event_matrix() -> None:
|
|
43
42
|
lines.append(f" - {mapping.log_key}")
|
44
43
|
if len(config.field_mappings) > 5:
|
45
44
|
lines.append(f" ... and {len(config.field_mappings) - 5} more")
|
46
|
-
|
45
|
+
|
47
46
|
# Show event sets
|
48
47
|
if config.event_sets:
|
49
48
|
lines.append(f" Event Sets ({len(config.event_sets)}):")
|
@@ -59,15 +58,15 @@ def show_event_matrix() -> None:
|
|
59
58
|
)
|
60
59
|
else:
|
61
60
|
lines.append("\n (No event sets registered)")
|
62
|
-
|
61
|
+
|
63
62
|
lines.append("\n" + "=" * 70)
|
64
|
-
|
63
|
+
|
65
64
|
# Show resolved state
|
66
65
|
if resolver._resolved:
|
67
66
|
lines.append("\nResolver State:")
|
68
67
|
lines.append(f" Total Field Mappings: {len(resolver._field_mappings)}")
|
69
68
|
lines.append(f" Total Event Sets: {len(resolver._event_sets)}")
|
70
|
-
|
69
|
+
|
71
70
|
# Show sample visual markers
|
72
71
|
if resolver._event_sets:
|
73
72
|
lines.append("\n Sample Visual Markers:")
|
@@ -79,6 +78,6 @@ def show_event_matrix() -> None:
|
|
79
78
|
lines.append(f" {marker} -> {key}")
|
80
79
|
else:
|
81
80
|
lines.append("\n (Resolver not yet initialized)")
|
82
|
-
|
81
|
+
|
83
82
|
# Log the complete display
|
84
|
-
logger.info("\n".join(lines))
|
83
|
+
logger.info("\n".join(lines))
|