provide-foundation 0.0.0.dev1__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 (93) hide show
  1. provide/foundation/__init__.py +29 -3
  2. provide/foundation/archive/operations.py +4 -6
  3. provide/foundation/cli/__init__.py +2 -2
  4. provide/foundation/cli/commands/deps.py +13 -7
  5. provide/foundation/cli/commands/logs/__init__.py +1 -1
  6. provide/foundation/cli/commands/logs/query.py +1 -1
  7. provide/foundation/cli/commands/logs/send.py +1 -1
  8. provide/foundation/cli/commands/logs/tail.py +1 -1
  9. provide/foundation/cli/decorators.py +11 -10
  10. provide/foundation/cli/main.py +1 -1
  11. provide/foundation/cli/testing.py +2 -35
  12. provide/foundation/cli/utils.py +21 -17
  13. provide/foundation/config/__init__.py +35 -2
  14. provide/foundation/config/converters.py +479 -0
  15. provide/foundation/config/defaults.py +67 -0
  16. provide/foundation/config/env.py +4 -19
  17. provide/foundation/config/loader.py +9 -3
  18. provide/foundation/console/input.py +5 -5
  19. provide/foundation/console/output.py +35 -13
  20. provide/foundation/context/__init__.py +8 -4
  21. provide/foundation/context/core.py +85 -109
  22. provide/foundation/crypto/certificates/operations.py +1 -1
  23. provide/foundation/errors/__init__.py +2 -3
  24. provide/foundation/errors/decorators.py +0 -231
  25. provide/foundation/errors/types.py +0 -97
  26. provide/foundation/file/directory.py +13 -22
  27. provide/foundation/file/lock.py +3 -1
  28. provide/foundation/hub/components.py +72 -384
  29. provide/foundation/hub/config.py +151 -0
  30. provide/foundation/hub/discovery.py +62 -0
  31. provide/foundation/hub/handlers.py +81 -0
  32. provide/foundation/hub/lifecycle.py +194 -0
  33. provide/foundation/hub/manager.py +4 -4
  34. provide/foundation/hub/processors.py +44 -0
  35. provide/foundation/integrations/__init__.py +11 -0
  36. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  37. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  38. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  39. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  40. provide/foundation/integrations/openobserve/config.py +37 -0
  41. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  42. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  43. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  44. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  45. provide/foundation/logger/config/logging.py +68 -298
  46. provide/foundation/logger/config/telemetry.py +41 -121
  47. provide/foundation/logger/setup/coordinator.py +1 -1
  48. provide/foundation/observability/__init__.py +2 -2
  49. provide/foundation/process/__init__.py +9 -0
  50. provide/foundation/process/exit.py +47 -0
  51. provide/foundation/process/lifecycle.py +33 -33
  52. provide/foundation/resilience/__init__.py +35 -0
  53. provide/foundation/resilience/circuit.py +164 -0
  54. provide/foundation/resilience/decorators.py +220 -0
  55. provide/foundation/resilience/fallback.py +193 -0
  56. provide/foundation/resilience/retry.py +325 -0
  57. provide/foundation/streams/config.py +79 -0
  58. provide/foundation/streams/console.py +7 -8
  59. provide/foundation/streams/core.py +6 -3
  60. provide/foundation/streams/file.py +12 -2
  61. provide/foundation/testing/__init__.py +7 -2
  62. provide/foundation/testing/cli.py +30 -17
  63. provide/foundation/testing/common/__init__.py +0 -2
  64. provide/foundation/testing/common/fixtures.py +0 -27
  65. provide/foundation/testing/file/content_fixtures.py +316 -0
  66. provide/foundation/testing/file/directory_fixtures.py +107 -0
  67. provide/foundation/testing/file/fixtures.py +45 -516
  68. provide/foundation/testing/file/special_fixtures.py +153 -0
  69. provide/foundation/testing/logger.py +76 -0
  70. provide/foundation/testing/process/async_fixtures.py +405 -0
  71. provide/foundation/testing/process/fixtures.py +50 -571
  72. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  73. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  74. provide/foundation/testing/threading/data_fixtures.py +99 -0
  75. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  76. provide/foundation/testing/threading/fixtures.py +34 -500
  77. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  78. provide/foundation/testing/time/fixtures.py +4 -4
  79. provide/foundation/tools/cache.py +8 -6
  80. provide/foundation/tools/downloader.py +23 -12
  81. provide/foundation/tracer/spans.py +2 -2
  82. provide/foundation/transport/config.py +26 -95
  83. provide/foundation/transport/middleware.py +30 -36
  84. provide/foundation/utils/deps.py +14 -12
  85. provide/foundation/utils/parsing.py +49 -4
  86. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +1 -1
  87. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/RECORD +93 -68
  88. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  89. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  90. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  91. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  92. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  93. {provide_foundation-0.0.0.dev1.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:
@@ -6,6 +6,8 @@ from pathlib import Path
6
6
  import shutil
7
7
  import tempfile
8
8
 
9
+ from provide.foundation.errors.decorators import with_error_handling
10
+ from provide.foundation.errors.handlers import error_boundary
9
11
  from provide.foundation.logger import get_logger
10
12
 
11
13
  log = get_logger(__name__)
@@ -80,17 +82,12 @@ def temp_dir(
80
82
  yield temp_path
81
83
  finally:
82
84
  if cleanup and temp_path and temp_path.exists():
83
- try:
85
+ with error_boundary(Exception, reraise=False):
84
86
  shutil.rmtree(temp_path)
85
87
  log.debug("Cleaned up temp directory", path=str(temp_path))
86
- except Exception as e:
87
- log.warning(
88
- "Failed to cleanup temp directory",
89
- path=str(temp_path),
90
- error=str(e),
91
- )
92
88
 
93
89
 
90
+ @with_error_handling(fallback=False, suppress=(FileNotFoundError,) if False else ())
94
91
  def safe_rmtree(
95
92
  path: Path | str,
96
93
  missing_ok: bool = True,
@@ -109,21 +106,15 @@ def safe_rmtree(
109
106
  """
110
107
  path = Path(path)
111
108
 
112
- try:
113
- if path.exists():
114
- shutil.rmtree(path)
115
- log.debug("Removed directory tree", path=str(path))
116
- return True
117
- elif missing_ok:
118
- log.debug("Directory already absent", path=str(path))
119
- return False
120
- else:
121
- raise FileNotFoundError(f"Directory does not exist: {path}")
122
- except Exception as e:
123
- if not path.exists() and missing_ok:
124
- return False
125
- log.error("Failed to remove directory tree", path=str(path), error=str(e))
126
- raise
109
+ if path.exists():
110
+ shutil.rmtree(path)
111
+ log.debug("Removed directory tree", path=str(path))
112
+ return True
113
+ elif missing_ok:
114
+ log.debug("Directory already absent", path=str(path))
115
+ return False
116
+ else:
117
+ raise FileNotFoundError(f"Directory does not exist: {path}")
127
118
 
128
119
 
129
120
  __all__ = [
@@ -4,6 +4,8 @@ import os
4
4
  from pathlib import Path
5
5
  import time
6
6
 
7
+ from provide.foundation.config.defaults import DEFAULT_FILE_LOCK_TIMEOUT
8
+ from provide.foundation.errors.decorators import with_error_handling
7
9
  from provide.foundation.errors.resources import LockError
8
10
  from provide.foundation.logger import get_logger
9
11
 
@@ -25,7 +27,7 @@ class FileLock:
25
27
  def __init__(
26
28
  self,
27
29
  path: Path | str,
28
- timeout: float = 10.0,
30
+ timeout: float = DEFAULT_FILE_LOCK_TIMEOUT,
29
31
  check_interval: float = 0.1,
30
32
  ) -> None:
31
33
  """Initialize file lock.