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.
Files changed (161) hide show
  1. provide/foundation/__init__.py +41 -23
  2. provide/foundation/archive/__init__.py +23 -0
  3. provide/foundation/archive/base.py +70 -0
  4. provide/foundation/archive/bzip2.py +157 -0
  5. provide/foundation/archive/gzip.py +159 -0
  6. provide/foundation/archive/operations.py +334 -0
  7. provide/foundation/archive/tar.py +164 -0
  8. provide/foundation/archive/zip.py +203 -0
  9. provide/foundation/cli/__init__.py +2 -2
  10. provide/foundation/cli/commands/deps.py +13 -7
  11. provide/foundation/cli/commands/logs/__init__.py +1 -1
  12. provide/foundation/cli/commands/logs/query.py +1 -1
  13. provide/foundation/cli/commands/logs/send.py +1 -1
  14. provide/foundation/cli/commands/logs/tail.py +1 -1
  15. provide/foundation/cli/decorators.py +11 -10
  16. provide/foundation/cli/main.py +1 -1
  17. provide/foundation/cli/testing.py +2 -35
  18. provide/foundation/cli/utils.py +21 -17
  19. provide/foundation/config/__init__.py +35 -2
  20. provide/foundation/config/base.py +2 -2
  21. provide/foundation/config/converters.py +479 -0
  22. provide/foundation/config/defaults.py +67 -0
  23. provide/foundation/config/env.py +4 -19
  24. provide/foundation/config/loader.py +9 -3
  25. provide/foundation/config/sync.py +19 -4
  26. provide/foundation/console/input.py +5 -5
  27. provide/foundation/console/output.py +35 -13
  28. provide/foundation/context/__init__.py +8 -4
  29. provide/foundation/context/core.py +85 -109
  30. provide/foundation/core.py +1 -2
  31. provide/foundation/crypto/__init__.py +2 -0
  32. provide/foundation/crypto/certificates/__init__.py +34 -0
  33. provide/foundation/crypto/certificates/base.py +173 -0
  34. provide/foundation/crypto/certificates/certificate.py +290 -0
  35. provide/foundation/crypto/certificates/factory.py +213 -0
  36. provide/foundation/crypto/certificates/generator.py +138 -0
  37. provide/foundation/crypto/certificates/loader.py +130 -0
  38. provide/foundation/crypto/certificates/operations.py +198 -0
  39. provide/foundation/crypto/certificates/trust.py +107 -0
  40. provide/foundation/errors/__init__.py +2 -3
  41. provide/foundation/errors/decorators.py +0 -231
  42. provide/foundation/errors/types.py +0 -97
  43. provide/foundation/eventsets/__init__.py +0 -0
  44. provide/foundation/eventsets/display.py +84 -0
  45. provide/foundation/eventsets/registry.py +160 -0
  46. provide/foundation/eventsets/resolver.py +192 -0
  47. provide/foundation/eventsets/sets/das.py +128 -0
  48. provide/foundation/eventsets/sets/database.py +125 -0
  49. provide/foundation/eventsets/sets/http.py +153 -0
  50. provide/foundation/eventsets/sets/llm.py +139 -0
  51. provide/foundation/eventsets/sets/task_queue.py +107 -0
  52. provide/foundation/eventsets/types.py +70 -0
  53. provide/foundation/file/directory.py +13 -22
  54. provide/foundation/file/lock.py +3 -1
  55. provide/foundation/hub/components.py +77 -515
  56. provide/foundation/hub/config.py +151 -0
  57. provide/foundation/hub/discovery.py +62 -0
  58. provide/foundation/hub/handlers.py +81 -0
  59. provide/foundation/hub/lifecycle.py +194 -0
  60. provide/foundation/hub/manager.py +4 -4
  61. provide/foundation/hub/processors.py +44 -0
  62. provide/foundation/integrations/__init__.py +11 -0
  63. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  64. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  65. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  66. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  67. provide/foundation/integrations/openobserve/config.py +37 -0
  68. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  70. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  71. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  72. provide/foundation/logger/__init__.py +3 -10
  73. provide/foundation/logger/config/logging.py +68 -298
  74. provide/foundation/logger/config/telemetry.py +41 -121
  75. provide/foundation/logger/core.py +0 -2
  76. provide/foundation/logger/custom_processors.py +1 -0
  77. provide/foundation/logger/factories.py +11 -2
  78. provide/foundation/logger/processors/main.py +20 -84
  79. provide/foundation/logger/setup/__init__.py +5 -1
  80. provide/foundation/logger/setup/coordinator.py +76 -24
  81. provide/foundation/logger/setup/processors.py +2 -9
  82. provide/foundation/logger/trace.py +27 -0
  83. provide/foundation/metrics/otel.py +10 -10
  84. provide/foundation/observability/__init__.py +2 -2
  85. provide/foundation/process/__init__.py +9 -0
  86. provide/foundation/process/exit.py +47 -0
  87. provide/foundation/process/lifecycle.py +115 -59
  88. provide/foundation/resilience/__init__.py +35 -0
  89. provide/foundation/resilience/circuit.py +164 -0
  90. provide/foundation/resilience/decorators.py +220 -0
  91. provide/foundation/resilience/fallback.py +193 -0
  92. provide/foundation/resilience/retry.py +325 -0
  93. provide/foundation/streams/config.py +79 -0
  94. provide/foundation/streams/console.py +7 -8
  95. provide/foundation/streams/core.py +6 -3
  96. provide/foundation/streams/file.py +12 -2
  97. provide/foundation/testing/__init__.py +84 -2
  98. provide/foundation/testing/archive/__init__.py +24 -0
  99. provide/foundation/testing/archive/fixtures.py +217 -0
  100. provide/foundation/testing/cli.py +30 -17
  101. provide/foundation/testing/common/__init__.py +32 -0
  102. provide/foundation/testing/common/fixtures.py +236 -0
  103. provide/foundation/testing/file/__init__.py +40 -0
  104. provide/foundation/testing/file/content_fixtures.py +316 -0
  105. provide/foundation/testing/file/directory_fixtures.py +107 -0
  106. provide/foundation/testing/file/fixtures.py +52 -0
  107. provide/foundation/testing/file/special_fixtures.py +153 -0
  108. provide/foundation/testing/logger.py +117 -11
  109. provide/foundation/testing/mocking/__init__.py +46 -0
  110. provide/foundation/testing/mocking/fixtures.py +331 -0
  111. provide/foundation/testing/process/__init__.py +48 -0
  112. provide/foundation/testing/process/async_fixtures.py +405 -0
  113. provide/foundation/testing/process/fixtures.py +56 -0
  114. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  115. provide/foundation/testing/threading/__init__.py +38 -0
  116. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  117. provide/foundation/testing/threading/data_fixtures.py +99 -0
  118. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  119. provide/foundation/testing/threading/fixtures.py +54 -0
  120. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  121. provide/foundation/testing/time/__init__.py +32 -0
  122. provide/foundation/testing/time/fixtures.py +409 -0
  123. provide/foundation/testing/transport/__init__.py +30 -0
  124. provide/foundation/testing/transport/fixtures.py +280 -0
  125. provide/foundation/tools/__init__.py +58 -0
  126. provide/foundation/tools/base.py +348 -0
  127. provide/foundation/tools/cache.py +268 -0
  128. provide/foundation/tools/downloader.py +224 -0
  129. provide/foundation/tools/installer.py +254 -0
  130. provide/foundation/tools/registry.py +223 -0
  131. provide/foundation/tools/resolver.py +321 -0
  132. provide/foundation/tools/verifier.py +186 -0
  133. provide/foundation/tracer/otel.py +7 -11
  134. provide/foundation/tracer/spans.py +2 -2
  135. provide/foundation/transport/__init__.py +155 -0
  136. provide/foundation/transport/base.py +171 -0
  137. provide/foundation/transport/client.py +266 -0
  138. provide/foundation/transport/config.py +140 -0
  139. provide/foundation/transport/errors.py +79 -0
  140. provide/foundation/transport/http.py +232 -0
  141. provide/foundation/transport/middleware.py +360 -0
  142. provide/foundation/transport/registry.py +167 -0
  143. provide/foundation/transport/types.py +45 -0
  144. provide/foundation/utils/deps.py +14 -12
  145. provide/foundation/utils/parsing.py +49 -4
  146. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
  147. provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
  148. provide/foundation/cli/commands/logs/generate_old.py +0 -569
  149. provide/foundation/crypto/certificates.py +0 -896
  150. provide/foundation/logger/emoji/__init__.py +0 -44
  151. provide/foundation/logger/emoji/matrix.py +0 -209
  152. provide/foundation/logger/emoji/sets.py +0 -458
  153. provide/foundation/logger/emoji/types.py +0 -56
  154. provide/foundation/logger/setup/emoji_resolver.py +0 -64
  155. provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
  156. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  157. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  158. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  159. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  160. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  161. {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()