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
@@ -0,0 +1,325 @@
1
+ """
2
+ Unified retry execution engine and policy configuration.
3
+
4
+ This module provides the core retry functionality used throughout foundation,
5
+ eliminating duplication between decorators and middleware.
6
+ """
7
+
8
+ import asyncio
9
+ import random
10
+ import time
11
+ from enum import Enum
12
+ from typing import Any, Callable, TypeVar
13
+
14
+ from attrs import define, field, validators
15
+
16
+ from provide.foundation.logger import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ class BackoffStrategy(str, Enum):
24
+ """Backoff strategies for retry delays."""
25
+
26
+ FIXED = "fixed" # Same delay every time
27
+ LINEAR = "linear" # Linear increase (delay * attempt)
28
+ EXPONENTIAL = "exponential" # Exponential increase (delay * 2^attempt)
29
+ FIBONACCI = "fibonacci" # Fibonacci sequence delays
30
+
31
+
32
+ @define(frozen=True, kw_only=True)
33
+ class RetryPolicy:
34
+ """
35
+ Configuration for retry behavior.
36
+
37
+ This policy can be used with both the @retry decorator and transport middleware,
38
+ providing a unified configuration model for all retry scenarios.
39
+
40
+ Attributes:
41
+ max_attempts: Maximum number of retry attempts (must be >= 1)
42
+ backoff: Backoff strategy to use for delays
43
+ base_delay: Base delay in seconds between retries
44
+ max_delay: Maximum delay in seconds (caps exponential growth)
45
+ jitter: Whether to add random jitter to delays (±25%)
46
+ retryable_errors: Tuple of exception types to retry (None = all)
47
+ retryable_status_codes: Set of HTTP status codes to retry (for middleware)
48
+ """
49
+
50
+ max_attempts: int = field(default=3, validator=validators.instance_of(int))
51
+ backoff: BackoffStrategy = field(default=BackoffStrategy.EXPONENTIAL)
52
+ base_delay: float = field(default=1.0, validator=validators.instance_of((int, float)))
53
+ max_delay: float = field(default=60.0, validator=validators.instance_of((int, float)))
54
+ jitter: bool = field(default=True)
55
+ retryable_errors: tuple[type[Exception], ...] | None = field(default=None)
56
+ retryable_status_codes: set[int] | None = field(default=None)
57
+
58
+ @max_attempts.validator
59
+ def _validate_max_attempts(self, attribute, value):
60
+ """Validate max_attempts is at least 1."""
61
+ if value < 1:
62
+ raise ValueError("max_attempts must be at least 1")
63
+
64
+ @base_delay.validator
65
+ def _validate_base_delay(self, attribute, value):
66
+ """Validate base_delay is positive."""
67
+ if value < 0:
68
+ raise ValueError("base_delay must be positive")
69
+
70
+ @max_delay.validator
71
+ def _validate_max_delay(self, attribute, value):
72
+ """Validate max_delay is positive and >= base_delay."""
73
+ if value < 0:
74
+ raise ValueError("max_delay must be positive")
75
+ if value < self.base_delay:
76
+ raise ValueError("max_delay must be >= base_delay")
77
+
78
+ def calculate_delay(self, attempt: int) -> float:
79
+ """
80
+ Calculate delay for a given attempt number.
81
+
82
+ Args:
83
+ attempt: Attempt number (1-based)
84
+
85
+ Returns:
86
+ Delay in seconds
87
+ """
88
+ if attempt <= 0:
89
+ return 0
90
+
91
+ if self.backoff == BackoffStrategy.FIXED:
92
+ delay = self.base_delay
93
+ elif self.backoff == BackoffStrategy.LINEAR:
94
+ delay = self.base_delay * attempt
95
+ elif self.backoff == BackoffStrategy.EXPONENTIAL:
96
+ delay = self.base_delay * (2 ** (attempt - 1))
97
+ elif self.backoff == BackoffStrategy.FIBONACCI:
98
+ # Calculate fibonacci number for attempt
99
+ a, b = 0, 1
100
+ for _ in range(attempt):
101
+ a, b = b, a + b
102
+ delay = self.base_delay * a
103
+ else:
104
+ delay = self.base_delay
105
+
106
+ # Cap at max delay
107
+ delay = min(delay, self.max_delay)
108
+
109
+ # Add jitter if configured (±25% random variation)
110
+ if self.jitter:
111
+ jitter_factor = 0.75 + (random.random() * 0.5)
112
+ delay *= jitter_factor
113
+
114
+ return delay
115
+
116
+ def should_retry(self, error: Exception, attempt: int) -> bool:
117
+ """
118
+ Determine if an error should be retried.
119
+
120
+ Args:
121
+ error: The exception that occurred
122
+ attempt: Current attempt number (1-based)
123
+
124
+ Returns:
125
+ True if should retry, False otherwise
126
+ """
127
+ # Check attempt limit
128
+ if attempt >= self.max_attempts:
129
+ return False
130
+
131
+ # Check error type if filter is configured
132
+ if self.retryable_errors is not None:
133
+ return isinstance(error, self.retryable_errors)
134
+
135
+ # Default to retry for any error
136
+ return True
137
+
138
+ def should_retry_response(self, response: Any, attempt: int) -> bool:
139
+ """
140
+ Check if HTTP response should be retried.
141
+
142
+ Args:
143
+ response: Response object with status attribute
144
+ attempt: Current attempt number (1-based)
145
+
146
+ Returns:
147
+ True if should retry, False otherwise
148
+ """
149
+ # Check attempt limit
150
+ if attempt >= self.max_attempts:
151
+ return False
152
+
153
+ # Check status code if configured
154
+ if self.retryable_status_codes is not None:
155
+ return getattr(response, 'status', None) in self.retryable_status_codes
156
+
157
+ # Default to no retry for responses
158
+ return False
159
+
160
+ def __str__(self) -> str:
161
+ """Human-readable string representation."""
162
+ return (
163
+ f"RetryPolicy(max_attempts={self.max_attempts}, "
164
+ f"backoff={self.backoff.value}, base_delay={self.base_delay}s)"
165
+ )
166
+
167
+
168
+ class RetryExecutor:
169
+ """
170
+ Unified retry execution engine.
171
+
172
+ This executor handles the actual retry loop logic for both sync and async
173
+ functions, using a RetryPolicy for configuration. It's used internally by
174
+ both the @retry decorator and RetryMiddleware.
175
+ """
176
+
177
+ def __init__(
178
+ self,
179
+ policy: RetryPolicy,
180
+ on_retry: Callable[[int, Exception], None] | None = None
181
+ ):
182
+ """
183
+ Initialize retry executor.
184
+
185
+ Args:
186
+ policy: Retry policy configuration
187
+ on_retry: Optional callback for retry events (attempt, error)
188
+ """
189
+ self.policy = policy
190
+ self.on_retry = on_retry
191
+
192
+ def execute_sync(self, func: Callable[..., T], *args, **kwargs) -> T:
193
+ """
194
+ Execute synchronous function with retry logic.
195
+
196
+ Args:
197
+ func: Function to execute
198
+ *args: Positional arguments for func
199
+ **kwargs: Keyword arguments for func
200
+
201
+ Returns:
202
+ Result from successful execution
203
+
204
+ Raises:
205
+ Last exception if all retries are exhausted
206
+ """
207
+ last_exception = None
208
+
209
+ for attempt in range(1, self.policy.max_attempts + 1):
210
+ try:
211
+ return func(*args, **kwargs)
212
+ except Exception as e:
213
+ last_exception = e
214
+
215
+ # Don't retry on last attempt - log and raise
216
+ if attempt >= self.policy.max_attempts:
217
+ logger.error(
218
+ f"All {self.policy.max_attempts} retry attempts failed",
219
+ attempts=self.policy.max_attempts,
220
+ error=str(e),
221
+ error_type=type(e).__name__,
222
+ )
223
+ raise
224
+
225
+ # Check if we should retry this error
226
+ if not self.policy.should_retry(e, attempt):
227
+ raise
228
+
229
+ # Calculate delay
230
+ delay = self.policy.calculate_delay(attempt)
231
+
232
+ # Log retry attempt
233
+ logger.info(
234
+ f"Retry {attempt}/{self.policy.max_attempts} after {delay:.2f}s",
235
+ attempt=attempt,
236
+ max_attempts=self.policy.max_attempts,
237
+ delay=delay,
238
+ error=str(e),
239
+ error_type=type(e).__name__,
240
+ )
241
+
242
+ # Call retry callback if provided
243
+ if self.on_retry:
244
+ try:
245
+ self.on_retry(attempt, e)
246
+ except Exception as callback_error:
247
+ logger.warning(
248
+ "Retry callback failed",
249
+ error=str(callback_error)
250
+ )
251
+
252
+ # Wait before retry
253
+ time.sleep(delay)
254
+
255
+ # Should never reach here, but for safety
256
+ raise last_exception
257
+
258
+ async def execute_async(self, func: Callable[..., T], *args, **kwargs) -> T:
259
+ """
260
+ Execute asynchronous function with retry logic.
261
+
262
+ Args:
263
+ func: Async function to execute
264
+ *args: Positional arguments for func
265
+ **kwargs: Keyword arguments for func
266
+
267
+ Returns:
268
+ Result from successful execution
269
+
270
+ Raises:
271
+ Last exception if all retries are exhausted
272
+ """
273
+ last_exception = None
274
+
275
+ for attempt in range(1, self.policy.max_attempts + 1):
276
+ try:
277
+ return await func(*args, **kwargs)
278
+ except Exception as e:
279
+ last_exception = e
280
+
281
+ # Don't retry on last attempt - log and raise
282
+ if attempt >= self.policy.max_attempts:
283
+ logger.error(
284
+ f"All {self.policy.max_attempts} retry attempts failed",
285
+ attempts=self.policy.max_attempts,
286
+ error=str(e),
287
+ error_type=type(e).__name__,
288
+ )
289
+ raise
290
+
291
+ # Check if we should retry this error
292
+ if not self.policy.should_retry(e, attempt):
293
+ raise
294
+
295
+ # Calculate delay
296
+ delay = self.policy.calculate_delay(attempt)
297
+
298
+ # Log retry attempt
299
+ logger.info(
300
+ f"Retry {attempt}/{self.policy.max_attempts} after {delay:.2f}s",
301
+ attempt=attempt,
302
+ max_attempts=self.policy.max_attempts,
303
+ delay=delay,
304
+ error=str(e),
305
+ error_type=type(e).__name__,
306
+ )
307
+
308
+ # Call retry callback if provided
309
+ if self.on_retry:
310
+ try:
311
+ if asyncio.iscoroutinefunction(self.on_retry):
312
+ await self.on_retry(attempt, e)
313
+ else:
314
+ self.on_retry(attempt, e)
315
+ except Exception as callback_error:
316
+ logger.warning(
317
+ "Retry callback failed",
318
+ error=str(callback_error)
319
+ )
320
+
321
+ # Wait before retry
322
+ await asyncio.sleep(delay)
323
+
324
+ # Should never reach here, but for safety
325
+ raise last_exception
@@ -0,0 +1,79 @@
1
+ """
2
+ Stream configuration for console output settings.
3
+
4
+ This module provides configuration for console stream behavior,
5
+ including color support and testing mode detection.
6
+ """
7
+
8
+ from attrs import define
9
+
10
+ from provide.foundation.config.env import RuntimeConfig
11
+ from provide.foundation.config.base import field
12
+ from provide.foundation.config.converters import parse_bool_extended
13
+
14
+
15
+ @define(slots=True, repr=False)
16
+ class StreamConfig(RuntimeConfig):
17
+ """Configuration for console stream output behavior."""
18
+
19
+ no_color: bool = field(
20
+ default=False,
21
+ env_var="NO_COLOR",
22
+ converter=parse_bool_extended,
23
+ description="Disable color output in console",
24
+ )
25
+
26
+ force_color: bool = field(
27
+ default=False,
28
+ env_var="FORCE_COLOR",
29
+ converter=parse_bool_extended,
30
+ description="Force color output even when not in TTY",
31
+ )
32
+
33
+ click_testing: bool = field(
34
+ default=False,
35
+ env_var="CLICK_TESTING",
36
+ converter=parse_bool_extended,
37
+ description="Indicates if running inside Click testing framework",
38
+ )
39
+
40
+
41
+ def supports_color(self) -> bool:
42
+ """
43
+ Determine if the console supports color output.
44
+
45
+ Returns:
46
+ True if color is supported, False otherwise
47
+ """
48
+ if self.no_color:
49
+ return False
50
+
51
+ if self.force_color:
52
+ return True
53
+
54
+ # Additional logic for TTY detection would go here
55
+ # For now, just return based on the flags
56
+ return not self.no_color
57
+
58
+
59
+ # Global instance for easy access
60
+ _stream_config: StreamConfig | None = None
61
+
62
+
63
+ def get_stream_config() -> StreamConfig:
64
+ """
65
+ Get the global stream configuration instance.
66
+
67
+ Returns:
68
+ StreamConfig instance loaded from environment
69
+ """
70
+ global _stream_config
71
+ if _stream_config is None:
72
+ _stream_config = StreamConfig.from_env()
73
+ return _stream_config
74
+
75
+
76
+ def reset_stream_config() -> None:
77
+ """Reset the global stream configuration (mainly for testing)."""
78
+ global _stream_config
79
+ _stream_config = None
@@ -9,6 +9,7 @@ Handles console-specific stream operations and formatting.
9
9
  import sys
10
10
  from typing import TextIO
11
11
 
12
+ from provide.foundation.streams.config import get_stream_config
12
13
  from provide.foundation.streams.core import get_log_stream
13
14
 
14
15
 
@@ -25,16 +26,14 @@ def is_tty() -> bool:
25
26
 
26
27
  def supports_color() -> bool:
27
28
  """Check if the current stream supports color output."""
28
- import os
29
-
30
- # Check NO_COLOR environment variable
31
- if os.getenv("NO_COLOR"):
29
+ config = get_stream_config()
30
+
31
+ if config.no_color:
32
32
  return False
33
-
34
- # Check FORCE_COLOR environment variable
35
- if os.getenv("FORCE_COLOR"):
33
+
34
+ if config.force_color:
36
35
  return True
37
-
36
+
38
37
  # Check if we're in a TTY
39
38
  return is_tty()
40
39
 
@@ -10,6 +10,8 @@ import sys
10
10
  import threading
11
11
  from typing import TextIO
12
12
 
13
+ from provide.foundation.streams.config import get_stream_config
14
+
13
15
  _PROVIDE_LOG_STREAM: TextIO = sys.stderr
14
16
  _LOG_FILE_HANDLE: TextIO | None = None
15
17
  _STREAM_LOCK = threading.Lock()
@@ -18,10 +20,11 @@ _STREAM_LOCK = threading.Lock()
18
20
  def _is_in_click_testing() -> bool:
19
21
  """Check if we're running inside Click's testing framework."""
20
22
  import inspect
21
- import os
22
-
23
+
24
+ config = get_stream_config()
25
+
23
26
  # Check environment variables for Click testing
24
- if os.getenv("CLICK_TESTING"):
27
+ if config.click_testing:
25
28
  return True
26
29
 
27
30
  # Check the call stack for Click's testing module or CLI integration tests
@@ -18,6 +18,16 @@ from provide.foundation.streams.core import (
18
18
  from provide.foundation.utils.streams import get_safe_stderr
19
19
 
20
20
 
21
+ def _safe_error_output(message: str) -> None:
22
+ """
23
+ Output error message to stderr using basic print to avoid circular dependencies.
24
+
25
+ This function intentionally uses print() instead of Foundation's perr() to prevent
26
+ circular import issues during stream initialization and teardown phases.
27
+ """
28
+ print(message, file=sys.stderr)
29
+
30
+
21
31
  def configure_file_logging(log_file_path: str | None) -> None:
22
32
  """
23
33
  Configure file logging if a path is provided.
@@ -56,7 +66,7 @@ def configure_file_logging(log_file_path: str | None) -> None:
56
66
  _PROVIDE_LOG_STREAM = _LOG_FILE_HANDLE
57
67
  except Exception as e:
58
68
  # Log error to stderr and fall back
59
- print(f"Failed to open log file {log_file_path}: {e}", file=sys.stderr)
69
+ _safe_error_output(f"Failed to open log file {log_file_path}: {e}")
60
70
  _PROVIDE_LOG_STREAM = get_safe_stderr()
61
71
  elif not is_test_stream:
62
72
  _PROVIDE_LOG_STREAM = get_safe_stderr()
@@ -71,7 +81,7 @@ def flush_log_streams() -> None:
71
81
  try:
72
82
  _LOG_FILE_HANDLE.flush()
73
83
  except Exception as e:
74
- print(f"Failed to flush log file handle: {e}", file=sys.stderr)
84
+ _safe_error_output(f"Failed to flush log file handle: {e}")
75
85
 
76
86
 
77
87
  def close_log_streams() -> None:
@@ -66,15 +66,20 @@ def __getattr__(name: str) -> Any:
66
66
  "isolated_cli_runner",
67
67
  "temp_config_file",
68
68
  "create_test_cli",
69
- "mock_logger",
70
69
  "CliTestCase",
70
+ "click_testing_mode",
71
71
  ]:
72
72
  import provide.foundation.testing.cli as cli_module
73
73
 
74
74
  return getattr(cli_module, name)
75
75
 
76
76
  # Logger testing utilities
77
- elif name in ["reset_foundation_setup_for_testing", "reset_foundation_state"]:
77
+ elif name in [
78
+ "reset_foundation_setup_for_testing",
79
+ "reset_foundation_state",
80
+ "mock_logger",
81
+ "mock_logger_factory",
82
+ ]:
78
83
  import provide.foundation.testing.logger as logger_module
79
84
 
80
85
  return getattr(logger_module, name)
@@ -93,6 +98,83 @@ def __getattr__(name: str) -> Any:
93
98
  import provide.foundation.testing.fixtures as fixtures_module
94
99
 
95
100
  return getattr(fixtures_module, name)
101
+
102
+ # Import submodules directly
103
+ elif name in ["archive", "common", "file", "process", "transport", "mocking", "time", "threading"]:
104
+ import importlib
105
+ return importlib.import_module(f"provide.foundation.testing.{name}")
106
+
107
+ # File testing utilities (backward compatibility)
108
+ elif name in [
109
+ "temp_directory",
110
+ "test_files_structure",
111
+ "temp_file",
112
+ "binary_file",
113
+ "nested_directory_structure",
114
+ "empty_directory",
115
+ "readonly_file",
116
+ ]:
117
+ import provide.foundation.testing.file.fixtures as file_module
118
+ return getattr(file_module, name)
119
+
120
+ # Process/async testing utilities (backward compatibility)
121
+ elif name in [
122
+ "clean_event_loop",
123
+ "async_timeout",
124
+ "mock_async_process",
125
+ "async_stream_reader",
126
+ "event_loop_policy",
127
+ "async_context_manager",
128
+ "async_iterator",
129
+ "async_queue",
130
+ "async_lock",
131
+ "mock_async_sleep",
132
+ ]:
133
+ import provide.foundation.testing.process.fixtures as process_module
134
+ return getattr(process_module, name)
135
+
136
+ # Common mock utilities (backward compatibility)
137
+ elif name in [
138
+ "mock_http_config",
139
+ "mock_telemetry_config",
140
+ "mock_config_source",
141
+ "mock_event_emitter",
142
+ "mock_transport",
143
+ "mock_metrics_collector",
144
+ "mock_cache",
145
+ "mock_database",
146
+ "mock_file_system",
147
+ "mock_subprocess",
148
+ ]:
149
+ import provide.foundation.testing.common.fixtures as common_module
150
+ return getattr(common_module, name)
151
+
152
+ # Transport/network testing utilities (backward compatibility)
153
+ elif name in [
154
+ "free_port",
155
+ "mock_server",
156
+ "httpx_mock_responses",
157
+ "mock_websocket",
158
+ "mock_dns_resolver",
159
+ "tcp_client_server",
160
+ "mock_ssl_context",
161
+ "network_timeout",
162
+ "mock_http_headers",
163
+ ]:
164
+ import provide.foundation.testing.transport.fixtures as transport_module
165
+ return getattr(transport_module, name)
166
+
167
+ # Archive testing utilities
168
+ elif name in [
169
+ "archive_test_content",
170
+ "large_file_for_compression",
171
+ "multi_format_archives",
172
+ "archive_with_permissions",
173
+ "corrupted_archives",
174
+ "archive_stress_test_files",
175
+ ]:
176
+ import provide.foundation.testing.archive.fixtures as archive_module
177
+ return getattr(archive_module, name)
96
178
 
97
179
  # Crypto fixtures (many fixtures)
98
180
  elif name in [
@@ -0,0 +1,24 @@
1
+ """
2
+ Archive testing fixtures for the provide-io ecosystem.
3
+
4
+ Standard fixtures for testing archive operations (tar, zip, gzip, bzip2)
5
+ across any project that depends on provide.foundation.
6
+ """
7
+
8
+ from provide.foundation.testing.archive.fixtures import (
9
+ archive_test_content,
10
+ large_file_for_compression,
11
+ multi_format_archives,
12
+ archive_with_permissions,
13
+ corrupted_archives,
14
+ archive_stress_test_files,
15
+ )
16
+
17
+ __all__ = [
18
+ "archive_test_content",
19
+ "large_file_for_compression",
20
+ "multi_format_archives",
21
+ "archive_with_permissions",
22
+ "corrupted_archives",
23
+ "archive_stress_test_files",
24
+ ]