provide-foundation 0.0.0.dev0__py3-none-any.whl → 0.0.0.dev2__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 +41 -23
- provide/foundation/archive/__init__.py +23 -0
- provide/foundation/archive/base.py +70 -0
- provide/foundation/archive/bzip2.py +157 -0
- provide/foundation/archive/gzip.py +159 -0
- provide/foundation/archive/operations.py +334 -0
- provide/foundation/archive/tar.py +164 -0
- provide/foundation/archive/zip.py +203 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +13 -7
- provide/foundation/cli/commands/logs/__init__.py +1 -1
- provide/foundation/cli/commands/logs/query.py +1 -1
- provide/foundation/cli/commands/logs/send.py +1 -1
- provide/foundation/cli/commands/logs/tail.py +1 -1
- provide/foundation/cli/decorators.py +11 -10
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -35
- provide/foundation/cli/utils.py +21 -17
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +479 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +4 -19
- provide/foundation/config/loader.py +9 -3
- provide/foundation/config/sync.py +19 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +35 -13
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +85 -109
- provide/foundation/core.py +1 -2
- provide/foundation/crypto/__init__.py +2 -0
- provide/foundation/crypto/certificates/__init__.py +34 -0
- provide/foundation/crypto/certificates/base.py +173 -0
- provide/foundation/crypto/certificates/certificate.py +290 -0
- provide/foundation/crypto/certificates/factory.py +213 -0
- provide/foundation/crypto/certificates/generator.py +138 -0
- provide/foundation/crypto/certificates/loader.py +130 -0
- provide/foundation/crypto/certificates/operations.py +198 -0
- provide/foundation/crypto/certificates/trust.py +107 -0
- provide/foundation/errors/__init__.py +2 -3
- provide/foundation/errors/decorators.py +0 -231
- provide/foundation/errors/types.py +0 -97
- provide/foundation/eventsets/__init__.py +0 -0
- provide/foundation/eventsets/display.py +84 -0
- provide/foundation/eventsets/registry.py +160 -0
- provide/foundation/eventsets/resolver.py +192 -0
- provide/foundation/eventsets/sets/das.py +128 -0
- provide/foundation/eventsets/sets/database.py +125 -0
- provide/foundation/eventsets/sets/http.py +153 -0
- provide/foundation/eventsets/sets/llm.py +139 -0
- provide/foundation/eventsets/sets/task_queue.py +107 -0
- provide/foundation/eventsets/types.py +70 -0
- provide/foundation/file/directory.py +13 -22
- provide/foundation/file/lock.py +3 -1
- provide/foundation/hub/components.py +77 -515
- provide/foundation/hub/config.py +151 -0
- provide/foundation/hub/discovery.py +62 -0
- provide/foundation/hub/handlers.py +81 -0
- provide/foundation/hub/lifecycle.py +194 -0
- provide/foundation/hub/manager.py +4 -4
- provide/foundation/hub/processors.py +44 -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 +12 -12
- provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
- provide/foundation/logger/__init__.py +3 -10
- provide/foundation/logger/config/logging.py +68 -298
- provide/foundation/logger/config/telemetry.py +41 -121
- provide/foundation/logger/core.py +0 -2
- provide/foundation/logger/custom_processors.py +1 -0
- provide/foundation/logger/factories.py +11 -2
- provide/foundation/logger/processors/main.py +20 -84
- provide/foundation/logger/setup/__init__.py +5 -1
- provide/foundation/logger/setup/coordinator.py +76 -24
- provide/foundation/logger/setup/processors.py +2 -9
- provide/foundation/logger/trace.py +27 -0
- provide/foundation/metrics/otel.py +10 -10
- provide/foundation/observability/__init__.py +2 -2
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +47 -0
- provide/foundation/process/lifecycle.py +115 -59
- provide/foundation/resilience/__init__.py +35 -0
- provide/foundation/resilience/circuit.py +164 -0
- provide/foundation/resilience/decorators.py +220 -0
- provide/foundation/resilience/fallback.py +193 -0
- provide/foundation/resilience/retry.py +325 -0
- provide/foundation/streams/config.py +79 -0
- provide/foundation/streams/console.py +7 -8
- provide/foundation/streams/core.py +6 -3
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +84 -2
- provide/foundation/testing/archive/__init__.py +24 -0
- provide/foundation/testing/archive/fixtures.py +217 -0
- provide/foundation/testing/cli.py +30 -17
- provide/foundation/testing/common/__init__.py +32 -0
- provide/foundation/testing/common/fixtures.py +236 -0
- provide/foundation/testing/file/__init__.py +40 -0
- provide/foundation/testing/file/content_fixtures.py +316 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +52 -0
- provide/foundation/testing/file/special_fixtures.py +153 -0
- provide/foundation/testing/logger.py +117 -11
- provide/foundation/testing/mocking/__init__.py +46 -0
- provide/foundation/testing/mocking/fixtures.py +331 -0
- provide/foundation/testing/process/__init__.py +48 -0
- provide/foundation/testing/process/async_fixtures.py +405 -0
- provide/foundation/testing/process/fixtures.py +56 -0
- provide/foundation/testing/process/subprocess_fixtures.py +209 -0
- provide/foundation/testing/threading/__init__.py +38 -0
- provide/foundation/testing/threading/basic_fixtures.py +101 -0
- provide/foundation/testing/threading/data_fixtures.py +99 -0
- provide/foundation/testing/threading/execution_fixtures.py +263 -0
- provide/foundation/testing/threading/fixtures.py +54 -0
- provide/foundation/testing/threading/sync_fixtures.py +97 -0
- provide/foundation/testing/time/__init__.py +32 -0
- provide/foundation/testing/time/fixtures.py +409 -0
- provide/foundation/testing/transport/__init__.py +30 -0
- provide/foundation/testing/transport/fixtures.py +280 -0
- provide/foundation/tools/__init__.py +58 -0
- provide/foundation/tools/base.py +348 -0
- provide/foundation/tools/cache.py +268 -0
- provide/foundation/tools/downloader.py +224 -0
- provide/foundation/tools/installer.py +254 -0
- provide/foundation/tools/registry.py +223 -0
- provide/foundation/tools/resolver.py +321 -0
- provide/foundation/tools/verifier.py +186 -0
- provide/foundation/tracer/otel.py +7 -11
- provide/foundation/tracer/spans.py +2 -2
- provide/foundation/transport/__init__.py +155 -0
- provide/foundation/transport/base.py +171 -0
- provide/foundation/transport/client.py +266 -0
- provide/foundation/transport/config.py +140 -0
- provide/foundation/transport/errors.py +79 -0
- provide/foundation/transport/http.py +232 -0
- provide/foundation/transport/middleware.py +360 -0
- provide/foundation/transport/registry.py +167 -0
- provide/foundation/transport/types.py +45 -0
- provide/foundation/utils/deps.py +14 -12
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
- provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
- provide/foundation/cli/commands/logs/generate_old.py +0 -569
- provide/foundation/crypto/certificates.py +0 -896
- provide/foundation/logger/emoji/__init__.py +0 -44
- provide/foundation/logger/emoji/matrix.py +0 -209
- provide/foundation/logger/emoji/sets.py +0 -458
- provide/foundation/logger/emoji/types.py +0 -56
- provide/foundation/logger/setup/emoji_resolver.py +0 -64
- provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/top_level.txt +0 -0
@@ -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,125 +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
147
|
|
271
148
|
def suppress_and_log(
|
272
149
|
*exceptions: type[Exception],
|
@@ -374,111 +251,3 @@ def fallback_on_error(
|
|
374
251
|
return decorator
|
375
252
|
|
376
253
|
|
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,103 +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
121
|
|
219
122
|
@define(kw_only=True, slots=True)
|
220
123
|
class ErrorResponse:
|
File without changes
|
@@ -0,0 +1,84 @@
|
|
1
|
+
"""
|
2
|
+
Event set display utilities for Foundation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from provide.foundation.logger import get_logger
|
6
|
+
|
7
|
+
from provide.foundation.eventsets.registry import get_registry, discover_event_sets
|
8
|
+
from provide.foundation.eventsets.resolver import get_resolver
|
9
|
+
|
10
|
+
logger = get_logger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
def show_event_matrix() -> None:
|
14
|
+
"""
|
15
|
+
Display the active event set configuration to the console.
|
16
|
+
Shows all registered event sets and their field mappings.
|
17
|
+
"""
|
18
|
+
# Ensure event sets are discovered
|
19
|
+
discover_event_sets()
|
20
|
+
|
21
|
+
registry = get_registry()
|
22
|
+
resolver = get_resolver()
|
23
|
+
|
24
|
+
# Force resolution to ensure everything is loaded
|
25
|
+
resolver.resolve()
|
26
|
+
|
27
|
+
lines: list[str] = ["Foundation Event Sets: Active Configuration"]
|
28
|
+
lines.append("=" * 70)
|
29
|
+
|
30
|
+
# Show registered event sets
|
31
|
+
event_sets = registry.list_event_sets()
|
32
|
+
if event_sets:
|
33
|
+
lines.append(f"\nRegistered Event Sets ({len(event_sets)}):")
|
34
|
+
for config in event_sets:
|
35
|
+
lines.append(f"\n {config.name} (priority: {config.priority})")
|
36
|
+
if config.description:
|
37
|
+
lines.append(f" {config.description}")
|
38
|
+
|
39
|
+
# Show field mappings
|
40
|
+
if config.field_mappings:
|
41
|
+
lines.append(f" Field Mappings ({len(config.field_mappings)}):")
|
42
|
+
for mapping in config.field_mappings[:5]: # Show first 5
|
43
|
+
lines.append(f" - {mapping.log_key}")
|
44
|
+
if len(config.field_mappings) > 5:
|
45
|
+
lines.append(f" ... and {len(config.field_mappings) - 5} more")
|
46
|
+
|
47
|
+
# Show event sets
|
48
|
+
if config.event_sets:
|
49
|
+
lines.append(f" Event Sets ({len(config.event_sets)}):")
|
50
|
+
for event_set in config.event_sets:
|
51
|
+
marker_count = len(event_set.visual_markers)
|
52
|
+
metadata_count = len(event_set.metadata_fields)
|
53
|
+
transform_count = len(event_set.transformations)
|
54
|
+
lines.append(
|
55
|
+
f" - {event_set.name}: "
|
56
|
+
f"{marker_count} markers, "
|
57
|
+
f"{metadata_count} metadata, "
|
58
|
+
f"{transform_count} transforms"
|
59
|
+
)
|
60
|
+
else:
|
61
|
+
lines.append("\n (No event sets registered)")
|
62
|
+
|
63
|
+
lines.append("\n" + "=" * 70)
|
64
|
+
|
65
|
+
# Show resolved state
|
66
|
+
if resolver._resolved:
|
67
|
+
lines.append("\nResolver State:")
|
68
|
+
lines.append(f" Total Field Mappings: {len(resolver._field_mappings)}")
|
69
|
+
lines.append(f" Total Event Sets: {len(resolver._event_sets)}")
|
70
|
+
|
71
|
+
# Show sample visual markers
|
72
|
+
if resolver._event_sets:
|
73
|
+
lines.append("\n Sample Visual Markers:")
|
74
|
+
for name, event_set in list(resolver._event_sets.items())[:3]:
|
75
|
+
if event_set.visual_markers:
|
76
|
+
sample_markers = list(event_set.visual_markers.items())[:3]
|
77
|
+
lines.append(f" {name}:")
|
78
|
+
for key, marker in sample_markers:
|
79
|
+
lines.append(f" {marker} -> {key}")
|
80
|
+
else:
|
81
|
+
lines.append("\n (Resolver not yet initialized)")
|
82
|
+
|
83
|
+
# Log the complete display
|
84
|
+
logger.info("\n".join(lines))
|
@@ -0,0 +1,160 @@
|
|
1
|
+
"""
|
2
|
+
Event set registry and discovery.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import importlib
|
6
|
+
import pkgutil
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
from provide.foundation.errors.resources import AlreadyExistsError, NotFoundError
|
10
|
+
from provide.foundation.hub.registry import Registry
|
11
|
+
from provide.foundation.logger.setup.coordinator import create_foundation_internal_logger
|
12
|
+
|
13
|
+
from provide.foundation.eventsets.types import EventSet
|
14
|
+
|
15
|
+
# Bootstrap logger that doesn't trigger full logger setup
|
16
|
+
logger = create_foundation_internal_logger()
|
17
|
+
|
18
|
+
|
19
|
+
class EventSetRegistry(Registry):
|
20
|
+
"""
|
21
|
+
Registry for event set definitions using foundation Registry.
|
22
|
+
|
23
|
+
Extends the foundation Registry to provide specialized
|
24
|
+
methods for event set registration and discovery.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def register_event_set(self, event_set: EventSet) -> None:
|
28
|
+
"""
|
29
|
+
Register an event set definition.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
event_set: The EventSet to register
|
33
|
+
|
34
|
+
Raises:
|
35
|
+
AlreadyExistsError: If an event set with this name already exists
|
36
|
+
"""
|
37
|
+
try:
|
38
|
+
self.register(event_set.name, event_set, "eventset", metadata={"priority": event_set.priority})
|
39
|
+
logger.debug(
|
40
|
+
"Registered event set",
|
41
|
+
name=event_set.name,
|
42
|
+
priority=event_set.priority,
|
43
|
+
field_count=len(event_set.field_mappings),
|
44
|
+
mapping_count=len(event_set.mappings)
|
45
|
+
)
|
46
|
+
except AlreadyExistsError:
|
47
|
+
logger.warning("Event set already registered", name=event_set.name)
|
48
|
+
raise
|
49
|
+
|
50
|
+
def get_event_set(self, name: str) -> EventSet:
|
51
|
+
"""
|
52
|
+
Retrieve an event set by name.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
name: The name of the event set
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
The EventSet
|
59
|
+
|
60
|
+
Raises:
|
61
|
+
NotFoundError: If no event set with this name exists
|
62
|
+
"""
|
63
|
+
event_set = self.get(name, "eventset")
|
64
|
+
if event_set is None:
|
65
|
+
raise NotFoundError(f"Event set '{name}' not found")
|
66
|
+
return event_set
|
67
|
+
|
68
|
+
def list_event_sets(self) -> list[EventSet]:
|
69
|
+
"""
|
70
|
+
List all registered event sets sorted by priority.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
List of EventSet objects sorted by descending priority
|
74
|
+
"""
|
75
|
+
names = self.list_dimension("eventset")
|
76
|
+
entries = [self.get_entry(name, "eventset") for name in names]
|
77
|
+
entries = [entry for entry in entries if entry is not None]
|
78
|
+
entries.sort(key=lambda e: e.metadata.get("priority", 0), reverse=True)
|
79
|
+
return [entry.value for entry in entries]
|
80
|
+
|
81
|
+
def discover_sets(self) -> None:
|
82
|
+
"""
|
83
|
+
Auto-discover and register event sets from the sets/ directory.
|
84
|
+
|
85
|
+
Imports all modules in the sets/ subdirectory and registers
|
86
|
+
any EVENT_SET constants found.
|
87
|
+
"""
|
88
|
+
sets_path = Path(__file__).parent / "sets"
|
89
|
+
if not sets_path.exists():
|
90
|
+
logger.debug("No sets directory found for auto-discovery")
|
91
|
+
return
|
92
|
+
|
93
|
+
for module_info in pkgutil.iter_modules([str(sets_path)]):
|
94
|
+
if module_info.ispkg:
|
95
|
+
continue
|
96
|
+
|
97
|
+
module_name = f"provide.foundation.eventsets.sets.{module_info.name}"
|
98
|
+
try:
|
99
|
+
module = importlib.import_module(module_name)
|
100
|
+
|
101
|
+
if hasattr(module, "EVENT_SET"):
|
102
|
+
event_set = getattr(module, "EVENT_SET")
|
103
|
+
if isinstance(event_set, EventSet):
|
104
|
+
try:
|
105
|
+
self.register_event_set(event_set)
|
106
|
+
logger.debug(
|
107
|
+
"Auto-discovered event set",
|
108
|
+
module=module_name,
|
109
|
+
name=event_set.name
|
110
|
+
)
|
111
|
+
except AlreadyExistsError:
|
112
|
+
logger.debug(
|
113
|
+
"Event set already registered during discovery",
|
114
|
+
module=module_name,
|
115
|
+
name=event_set.name
|
116
|
+
)
|
117
|
+
else:
|
118
|
+
logger.warning(
|
119
|
+
"EVENT_SET is not an EventSet",
|
120
|
+
module=module_name,
|
121
|
+
type=type(event_set).__name__
|
122
|
+
)
|
123
|
+
|
124
|
+
except ImportError as e:
|
125
|
+
logger.debug(
|
126
|
+
"Failed to import event set module",
|
127
|
+
module=module_name,
|
128
|
+
error=str(e)
|
129
|
+
)
|
130
|
+
except Exception as e:
|
131
|
+
logger.warning(
|
132
|
+
"Error during event set discovery",
|
133
|
+
module=module_name,
|
134
|
+
error=str(e),
|
135
|
+
error_type=type(e).__name__
|
136
|
+
)
|
137
|
+
|
138
|
+
|
139
|
+
# Global registry instance
|
140
|
+
_registry = EventSetRegistry()
|
141
|
+
|
142
|
+
|
143
|
+
def get_registry() -> EventSetRegistry:
|
144
|
+
"""Get the global event set registry instance."""
|
145
|
+
return _registry
|
146
|
+
|
147
|
+
|
148
|
+
def register_event_set(event_set: EventSet) -> None:
|
149
|
+
"""
|
150
|
+
Register an event set in the global registry.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
event_set: The EventSet to register
|
154
|
+
"""
|
155
|
+
_registry.register_event_set(event_set)
|
156
|
+
|
157
|
+
|
158
|
+
def discover_event_sets() -> None:
|
159
|
+
"""Auto-discover and register all event sets."""
|
160
|
+
_registry.discover_sets()
|