provide-foundation 0.0.0.dev2__py3-none-any.whl → 0.0.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- provide/foundation/__init__.py +20 -20
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +90 -91
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +4 -4
- provide/foundation/cli/commands/logs/__init__.py +2 -2
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +3 -3
- provide/foundation/cli/commands/logs/send.py +2 -2
- provide/foundation/cli/commands/logs/tail.py +2 -2
- provide/foundation/cli/decorators.py +0 -1
- provide/foundation/cli/testing.py +0 -5
- provide/foundation/cli/utils.py +1 -2
- provide/foundation/config/__init__.py +19 -19
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +81 -83
- provide/foundation/config/defaults.py +1 -1
- provide/foundation/config/env.py +2 -1
- provide/foundation/config/loader.py +1 -1
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/output.py +7 -7
- provide/foundation/context/core.py +19 -17
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -2
- provide/foundation/errors/decorators.py +0 -3
- provide/foundation/errors/types.py +0 -1
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +1 -1
- provide/foundation/file/lock.py +2 -3
- provide/foundation/hub/components.py +19 -21
- provide/foundation/hub/config.py +25 -19
- provide/foundation/hub/discovery.py +5 -4
- provide/foundation/hub/handlers.py +13 -5
- provide/foundation/hub/lifecycle.py +10 -9
- provide/foundation/hub/manager.py +3 -0
- provide/foundation/hub/processors.py +8 -3
- provide/foundation/integrations/__init__.py +1 -1
- provide/foundation/integrations/openobserve/client.py +2 -2
- provide/foundation/integrations/openobserve/commands.py +9 -9
- provide/foundation/integrations/openobserve/config.py +2 -2
- provide/foundation/integrations/openobserve/otlp.py +2 -2
- provide/foundation/integrations/openobserve/search.py +1 -2
- provide/foundation/integrations/openobserve/streaming.py +1 -1
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +19 -19
- provide/foundation/logger/config/telemetry.py +11 -13
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +38 -24
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +1 -1
- provide/foundation/process/__init__.py +1 -1
- provide/foundation/process/exit.py +6 -5
- provide/foundation/process/lifecycle.py +41 -18
- provide/foundation/resilience/__init__.py +6 -5
- provide/foundation/resilience/circuit.py +32 -30
- provide/foundation/resilience/decorators.py +58 -42
- provide/foundation/resilience/fallback.py +55 -40
- provide/foundation/resilience/retry.py +67 -65
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +8 -9
- provide/foundation/streams/console.py +3 -3
- provide/foundation/streams/core.py +2 -2
- provide/foundation/streams/file.py +1 -1
- provide/foundation/testing/__init__.py +22 -7
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +3 -6
- provide/foundation/testing/common/__init__.py +13 -13
- provide/foundation/testing/common/fixtures.py +27 -30
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +65 -92
- provide/foundation/testing/file/directory_fixtures.py +19 -19
- provide/foundation/testing/file/fixtures.py +14 -17
- provide/foundation/testing/file/special_fixtures.py +34 -42
- provide/foundation/testing/logger.py +28 -23
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +89 -80
- provide/foundation/testing/process/fixtures.py +11 -13
- provide/foundation/testing/process/subprocess_fixtures.py +41 -40
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +21 -17
- provide/foundation/testing/threading/data_fixtures.py +18 -16
- provide/foundation/testing/threading/execution_fixtures.py +67 -52
- provide/foundation/testing/threading/fixtures.py +10 -14
- provide/foundation/testing/threading/sync_fixtures.py +21 -18
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +91 -79
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +62 -69
- provide/foundation/tools/downloader.py +51 -56
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +1 -13
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +11 -13
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +86 -81
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +3 -2
- provide/foundation/utils/parsing.py +7 -7
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -6,9 +6,9 @@ eliminating duplication between decorators and middleware.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
import asyncio
|
9
|
+
from enum import Enum
|
9
10
|
import random
|
10
11
|
import time
|
11
|
-
from enum import Enum
|
12
12
|
from typing import Any, Callable, TypeVar
|
13
13
|
|
14
14
|
from attrs import define, field, validators
|
@@ -22,7 +22,7 @@ T = TypeVar("T")
|
|
22
22
|
|
23
23
|
class BackoffStrategy(str, Enum):
|
24
24
|
"""Backoff strategies for retry delays."""
|
25
|
-
|
25
|
+
|
26
26
|
FIXED = "fixed" # Same delay every time
|
27
27
|
LINEAR = "linear" # Linear increase (delay * attempt)
|
28
28
|
EXPONENTIAL = "exponential" # Exponential increase (delay * 2^attempt)
|
@@ -33,10 +33,10 @@ class BackoffStrategy(str, Enum):
|
|
33
33
|
class RetryPolicy:
|
34
34
|
"""
|
35
35
|
Configuration for retry behavior.
|
36
|
-
|
36
|
+
|
37
37
|
This policy can be used with both the @retry decorator and transport middleware,
|
38
38
|
providing a unified configuration model for all retry scenarios.
|
39
|
-
|
39
|
+
|
40
40
|
Attributes:
|
41
41
|
max_attempts: Maximum number of retry attempts (must be >= 1)
|
42
42
|
backoff: Backoff strategy to use for delays
|
@@ -46,48 +46,52 @@ class RetryPolicy:
|
|
46
46
|
retryable_errors: Tuple of exception types to retry (None = all)
|
47
47
|
retryable_status_codes: Set of HTTP status codes to retry (for middleware)
|
48
48
|
"""
|
49
|
-
|
49
|
+
|
50
50
|
max_attempts: int = field(default=3, validator=validators.instance_of(int))
|
51
51
|
backoff: BackoffStrategy = field(default=BackoffStrategy.EXPONENTIAL)
|
52
|
-
base_delay: float = field(
|
53
|
-
|
52
|
+
base_delay: float = field(
|
53
|
+
default=1.0, validator=validators.instance_of((int, float))
|
54
|
+
)
|
55
|
+
max_delay: float = field(
|
56
|
+
default=60.0, validator=validators.instance_of((int, float))
|
57
|
+
)
|
54
58
|
jitter: bool = field(default=True)
|
55
59
|
retryable_errors: tuple[type[Exception], ...] | None = field(default=None)
|
56
60
|
retryable_status_codes: set[int] | None = field(default=None)
|
57
|
-
|
61
|
+
|
58
62
|
@max_attempts.validator
|
59
|
-
def _validate_max_attempts(self, attribute, value):
|
63
|
+
def _validate_max_attempts(self, attribute: object, value: int) -> None:
|
60
64
|
"""Validate max_attempts is at least 1."""
|
61
65
|
if value < 1:
|
62
66
|
raise ValueError("max_attempts must be at least 1")
|
63
|
-
|
67
|
+
|
64
68
|
@base_delay.validator
|
65
|
-
def _validate_base_delay(self, attribute, value):
|
69
|
+
def _validate_base_delay(self, attribute: object, value: float) -> None:
|
66
70
|
"""Validate base_delay is positive."""
|
67
71
|
if value < 0:
|
68
72
|
raise ValueError("base_delay must be positive")
|
69
|
-
|
73
|
+
|
70
74
|
@max_delay.validator
|
71
|
-
def _validate_max_delay(self, attribute, value):
|
75
|
+
def _validate_max_delay(self, attribute: object, value: float) -> None:
|
72
76
|
"""Validate max_delay is positive and >= base_delay."""
|
73
77
|
if value < 0:
|
74
78
|
raise ValueError("max_delay must be positive")
|
75
79
|
if value < self.base_delay:
|
76
80
|
raise ValueError("max_delay must be >= base_delay")
|
77
|
-
|
81
|
+
|
78
82
|
def calculate_delay(self, attempt: int) -> float:
|
79
83
|
"""
|
80
84
|
Calculate delay for a given attempt number.
|
81
|
-
|
85
|
+
|
82
86
|
Args:
|
83
87
|
attempt: Attempt number (1-based)
|
84
|
-
|
88
|
+
|
85
89
|
Returns:
|
86
90
|
Delay in seconds
|
87
91
|
"""
|
88
92
|
if attempt <= 0:
|
89
93
|
return 0
|
90
|
-
|
94
|
+
|
91
95
|
if self.backoff == BackoffStrategy.FIXED:
|
92
96
|
delay = self.base_delay
|
93
97
|
elif self.backoff == BackoffStrategy.LINEAR:
|
@@ -102,61 +106,61 @@ class RetryPolicy:
|
|
102
106
|
delay = self.base_delay * a
|
103
107
|
else:
|
104
108
|
delay = self.base_delay
|
105
|
-
|
109
|
+
|
106
110
|
# Cap at max delay
|
107
111
|
delay = min(delay, self.max_delay)
|
108
|
-
|
112
|
+
|
109
113
|
# Add jitter if configured (±25% random variation)
|
110
114
|
if self.jitter:
|
111
115
|
jitter_factor = 0.75 + (random.random() * 0.5)
|
112
116
|
delay *= jitter_factor
|
113
|
-
|
117
|
+
|
114
118
|
return delay
|
115
|
-
|
119
|
+
|
116
120
|
def should_retry(self, error: Exception, attempt: int) -> bool:
|
117
121
|
"""
|
118
122
|
Determine if an error should be retried.
|
119
|
-
|
123
|
+
|
120
124
|
Args:
|
121
125
|
error: The exception that occurred
|
122
126
|
attempt: Current attempt number (1-based)
|
123
|
-
|
127
|
+
|
124
128
|
Returns:
|
125
129
|
True if should retry, False otherwise
|
126
130
|
"""
|
127
131
|
# Check attempt limit
|
128
132
|
if attempt >= self.max_attempts:
|
129
133
|
return False
|
130
|
-
|
134
|
+
|
131
135
|
# Check error type if filter is configured
|
132
136
|
if self.retryable_errors is not None:
|
133
137
|
return isinstance(error, self.retryable_errors)
|
134
|
-
|
138
|
+
|
135
139
|
# Default to retry for any error
|
136
140
|
return True
|
137
|
-
|
141
|
+
|
138
142
|
def should_retry_response(self, response: Any, attempt: int) -> bool:
|
139
143
|
"""
|
140
144
|
Check if HTTP response should be retried.
|
141
|
-
|
145
|
+
|
142
146
|
Args:
|
143
147
|
response: Response object with status attribute
|
144
148
|
attempt: Current attempt number (1-based)
|
145
|
-
|
149
|
+
|
146
150
|
Returns:
|
147
151
|
True if should retry, False otherwise
|
148
152
|
"""
|
149
153
|
# Check attempt limit
|
150
154
|
if attempt >= self.max_attempts:
|
151
155
|
return False
|
152
|
-
|
156
|
+
|
153
157
|
# Check status code if configured
|
154
158
|
if self.retryable_status_codes is not None:
|
155
|
-
return getattr(response,
|
156
|
-
|
159
|
+
return getattr(response, "status", None) in self.retryable_status_codes
|
160
|
+
|
157
161
|
# Default to no retry for responses
|
158
162
|
return False
|
159
|
-
|
163
|
+
|
160
164
|
def __str__(self) -> str:
|
161
165
|
"""Human-readable string representation."""
|
162
166
|
return (
|
@@ -168,50 +172,50 @@ class RetryPolicy:
|
|
168
172
|
class RetryExecutor:
|
169
173
|
"""
|
170
174
|
Unified retry execution engine.
|
171
|
-
|
175
|
+
|
172
176
|
This executor handles the actual retry loop logic for both sync and async
|
173
177
|
functions, using a RetryPolicy for configuration. It's used internally by
|
174
178
|
both the @retry decorator and RetryMiddleware.
|
175
179
|
"""
|
176
|
-
|
180
|
+
|
177
181
|
def __init__(
|
178
182
|
self,
|
179
183
|
policy: RetryPolicy,
|
180
|
-
on_retry: Callable[[int, Exception], None] | None = None
|
184
|
+
on_retry: Callable[[int, Exception], None] | None = None,
|
181
185
|
):
|
182
186
|
"""
|
183
187
|
Initialize retry executor.
|
184
|
-
|
188
|
+
|
185
189
|
Args:
|
186
190
|
policy: Retry policy configuration
|
187
191
|
on_retry: Optional callback for retry events (attempt, error)
|
188
192
|
"""
|
189
193
|
self.policy = policy
|
190
194
|
self.on_retry = on_retry
|
191
|
-
|
195
|
+
|
192
196
|
def execute_sync(self, func: Callable[..., T], *args, **kwargs) -> T:
|
193
197
|
"""
|
194
198
|
Execute synchronous function with retry logic.
|
195
|
-
|
199
|
+
|
196
200
|
Args:
|
197
201
|
func: Function to execute
|
198
202
|
*args: Positional arguments for func
|
199
203
|
**kwargs: Keyword arguments for func
|
200
|
-
|
204
|
+
|
201
205
|
Returns:
|
202
206
|
Result from successful execution
|
203
|
-
|
207
|
+
|
204
208
|
Raises:
|
205
209
|
Last exception if all retries are exhausted
|
206
210
|
"""
|
207
211
|
last_exception = None
|
208
|
-
|
212
|
+
|
209
213
|
for attempt in range(1, self.policy.max_attempts + 1):
|
210
214
|
try:
|
211
215
|
return func(*args, **kwargs)
|
212
216
|
except Exception as e:
|
213
217
|
last_exception = e
|
214
|
-
|
218
|
+
|
215
219
|
# Don't retry on last attempt - log and raise
|
216
220
|
if attempt >= self.policy.max_attempts:
|
217
221
|
logger.error(
|
@@ -221,14 +225,14 @@ class RetryExecutor:
|
|
221
225
|
error_type=type(e).__name__,
|
222
226
|
)
|
223
227
|
raise
|
224
|
-
|
228
|
+
|
225
229
|
# Check if we should retry this error
|
226
230
|
if not self.policy.should_retry(e, attempt):
|
227
231
|
raise
|
228
|
-
|
232
|
+
|
229
233
|
# Calculate delay
|
230
234
|
delay = self.policy.calculate_delay(attempt)
|
231
|
-
|
235
|
+
|
232
236
|
# Log retry attempt
|
233
237
|
logger.info(
|
234
238
|
f"Retry {attempt}/{self.policy.max_attempts} after {delay:.2f}s",
|
@@ -238,46 +242,45 @@ class RetryExecutor:
|
|
238
242
|
error=str(e),
|
239
243
|
error_type=type(e).__name__,
|
240
244
|
)
|
241
|
-
|
245
|
+
|
242
246
|
# Call retry callback if provided
|
243
247
|
if self.on_retry:
|
244
248
|
try:
|
245
249
|
self.on_retry(attempt, e)
|
246
250
|
except Exception as callback_error:
|
247
251
|
logger.warning(
|
248
|
-
"Retry callback failed",
|
249
|
-
error=str(callback_error)
|
252
|
+
"Retry callback failed", error=str(callback_error)
|
250
253
|
)
|
251
|
-
|
254
|
+
|
252
255
|
# Wait before retry
|
253
256
|
time.sleep(delay)
|
254
|
-
|
257
|
+
|
255
258
|
# Should never reach here, but for safety
|
256
259
|
raise last_exception
|
257
|
-
|
260
|
+
|
258
261
|
async def execute_async(self, func: Callable[..., T], *args, **kwargs) -> T:
|
259
262
|
"""
|
260
263
|
Execute asynchronous function with retry logic.
|
261
|
-
|
264
|
+
|
262
265
|
Args:
|
263
266
|
func: Async function to execute
|
264
267
|
*args: Positional arguments for func
|
265
268
|
**kwargs: Keyword arguments for func
|
266
|
-
|
269
|
+
|
267
270
|
Returns:
|
268
271
|
Result from successful execution
|
269
|
-
|
272
|
+
|
270
273
|
Raises:
|
271
274
|
Last exception if all retries are exhausted
|
272
275
|
"""
|
273
276
|
last_exception = None
|
274
|
-
|
277
|
+
|
275
278
|
for attempt in range(1, self.policy.max_attempts + 1):
|
276
279
|
try:
|
277
280
|
return await func(*args, **kwargs)
|
278
281
|
except Exception as e:
|
279
282
|
last_exception = e
|
280
|
-
|
283
|
+
|
281
284
|
# Don't retry on last attempt - log and raise
|
282
285
|
if attempt >= self.policy.max_attempts:
|
283
286
|
logger.error(
|
@@ -287,14 +290,14 @@ class RetryExecutor:
|
|
287
290
|
error_type=type(e).__name__,
|
288
291
|
)
|
289
292
|
raise
|
290
|
-
|
293
|
+
|
291
294
|
# Check if we should retry this error
|
292
295
|
if not self.policy.should_retry(e, attempt):
|
293
296
|
raise
|
294
|
-
|
297
|
+
|
295
298
|
# Calculate delay
|
296
299
|
delay = self.policy.calculate_delay(attempt)
|
297
|
-
|
300
|
+
|
298
301
|
# Log retry attempt
|
299
302
|
logger.info(
|
300
303
|
f"Retry {attempt}/{self.policy.max_attempts} after {delay:.2f}s",
|
@@ -304,7 +307,7 @@ class RetryExecutor:
|
|
304
307
|
error=str(e),
|
305
308
|
error_type=type(e).__name__,
|
306
309
|
)
|
307
|
-
|
310
|
+
|
308
311
|
# Call retry callback if provided
|
309
312
|
if self.on_retry:
|
310
313
|
try:
|
@@ -314,12 +317,11 @@ class RetryExecutor:
|
|
314
317
|
self.on_retry(attempt, e)
|
315
318
|
except Exception as callback_error:
|
316
319
|
logger.warning(
|
317
|
-
"Retry callback failed",
|
318
|
-
error=str(callback_error)
|
320
|
+
"Retry callback failed", error=str(callback_error)
|
319
321
|
)
|
320
|
-
|
322
|
+
|
321
323
|
# Wait before retry
|
322
324
|
await asyncio.sleep(delay)
|
323
|
-
|
325
|
+
|
324
326
|
# Should never reach here, but for safety
|
325
|
-
raise last_exception
|
327
|
+
raise last_exception
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"""
|
2
|
+
Serialization utilities for Foundation.
|
3
|
+
|
4
|
+
Provides consistent serialization handling with validation,
|
5
|
+
testing support, and integration with Foundation's configuration system.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from provide.foundation.serialization.core import (
|
9
|
+
provide_dumps,
|
10
|
+
provide_loads,
|
11
|
+
)
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"provide_dumps",
|
15
|
+
"provide_loads",
|
16
|
+
]
|
@@ -0,0 +1,70 @@
|
|
1
|
+
"""Core serialization utilities for Foundation."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from provide.foundation.errors import ValidationError
|
7
|
+
|
8
|
+
|
9
|
+
def provide_dumps(
|
10
|
+
obj: Any,
|
11
|
+
*,
|
12
|
+
ensure_ascii: bool = False,
|
13
|
+
indent: int | None = None,
|
14
|
+
sort_keys: bool = False,
|
15
|
+
) -> str:
|
16
|
+
"""
|
17
|
+
Serialize object to JSON string with Foundation tracking.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
obj: Object to serialize
|
21
|
+
ensure_ascii: If True, non-ASCII characters are escaped
|
22
|
+
indent: Number of spaces for indentation (None for compact)
|
23
|
+
sort_keys: Whether to sort dictionary keys
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
JSON string representation
|
27
|
+
|
28
|
+
Raises:
|
29
|
+
ValidationError: If object cannot be serialized
|
30
|
+
|
31
|
+
Example:
|
32
|
+
>>> provide_dumps({"key": "value"})
|
33
|
+
'{"key": "value"}'
|
34
|
+
>>> provide_dumps({"b": 1, "a": 2}, sort_keys=True, indent=2)
|
35
|
+
'{\\n "a": 2,\\n "b": 1\\n}'
|
36
|
+
"""
|
37
|
+
try:
|
38
|
+
return json.dumps(
|
39
|
+
obj, ensure_ascii=ensure_ascii, indent=indent, sort_keys=sort_keys
|
40
|
+
)
|
41
|
+
except (TypeError, ValueError) as e:
|
42
|
+
raise ValidationError(f"Cannot serialize object to JSON: {e}") from e
|
43
|
+
|
44
|
+
|
45
|
+
def provide_loads(s: str) -> Any:
|
46
|
+
"""
|
47
|
+
Deserialize JSON string to Python object with Foundation tracking.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
s: JSON string to deserialize
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Deserialized Python object
|
54
|
+
|
55
|
+
Raises:
|
56
|
+
ValidationError: If string is not valid JSON
|
57
|
+
|
58
|
+
Example:
|
59
|
+
>>> provide_loads('{"key": "value"}')
|
60
|
+
{'key': 'value'}
|
61
|
+
>>> provide_loads('[1, 2, 3]')
|
62
|
+
[1, 2, 3]
|
63
|
+
"""
|
64
|
+
if not isinstance(s, str):
|
65
|
+
raise ValidationError("Input must be a string")
|
66
|
+
|
67
|
+
try:
|
68
|
+
return json.loads(s)
|
69
|
+
except json.JSONDecodeError as e:
|
70
|
+
raise ValidationError(f"Invalid JSON string: {e}") from e
|
@@ -7,9 +7,9 @@ including color support and testing mode detection.
|
|
7
7
|
|
8
8
|
from attrs import define
|
9
9
|
|
10
|
-
from provide.foundation.config.env import RuntimeConfig
|
11
10
|
from provide.foundation.config.base import field
|
12
11
|
from provide.foundation.config.converters import parse_bool_extended
|
12
|
+
from provide.foundation.config.env import RuntimeConfig
|
13
13
|
|
14
14
|
|
15
15
|
@define(slots=True, repr=False)
|
@@ -22,35 +22,34 @@ class StreamConfig(RuntimeConfig):
|
|
22
22
|
converter=parse_bool_extended,
|
23
23
|
description="Disable color output in console",
|
24
24
|
)
|
25
|
-
|
25
|
+
|
26
26
|
force_color: bool = field(
|
27
27
|
default=False,
|
28
28
|
env_var="FORCE_COLOR",
|
29
29
|
converter=parse_bool_extended,
|
30
30
|
description="Force color output even when not in TTY",
|
31
31
|
)
|
32
|
-
|
32
|
+
|
33
33
|
click_testing: bool = field(
|
34
34
|
default=False,
|
35
35
|
env_var="CLICK_TESTING",
|
36
36
|
converter=parse_bool_extended,
|
37
37
|
description="Indicates if running inside Click testing framework",
|
38
38
|
)
|
39
|
-
|
40
39
|
|
41
40
|
def supports_color(self) -> bool:
|
42
41
|
"""
|
43
42
|
Determine if the console supports color output.
|
44
|
-
|
43
|
+
|
45
44
|
Returns:
|
46
45
|
True if color is supported, False otherwise
|
47
46
|
"""
|
48
47
|
if self.no_color:
|
49
48
|
return False
|
50
|
-
|
49
|
+
|
51
50
|
if self.force_color:
|
52
51
|
return True
|
53
|
-
|
52
|
+
|
54
53
|
# Additional logic for TTY detection would go here
|
55
54
|
# For now, just return based on the flags
|
56
55
|
return not self.no_color
|
@@ -63,7 +62,7 @@ _stream_config: StreamConfig | None = None
|
|
63
62
|
def get_stream_config() -> StreamConfig:
|
64
63
|
"""
|
65
64
|
Get the global stream configuration instance.
|
66
|
-
|
65
|
+
|
67
66
|
Returns:
|
68
67
|
StreamConfig instance loaded from environment
|
69
68
|
"""
|
@@ -76,4 +75,4 @@ def get_stream_config() -> StreamConfig:
|
|
76
75
|
def reset_stream_config() -> None:
|
77
76
|
"""Reset the global stream configuration (mainly for testing)."""
|
78
77
|
global _stream_config
|
79
|
-
_stream_config = None
|
78
|
+
_stream_config = None
|
@@ -27,13 +27,13 @@ def is_tty() -> bool:
|
|
27
27
|
def supports_color() -> bool:
|
28
28
|
"""Check if the current stream supports color output."""
|
29
29
|
config = get_stream_config()
|
30
|
-
|
30
|
+
|
31
31
|
if config.no_color:
|
32
32
|
return False
|
33
|
-
|
33
|
+
|
34
34
|
if config.force_color:
|
35
35
|
return True
|
36
|
-
|
36
|
+
|
37
37
|
# Check if we're in a TTY
|
38
38
|
return is_tty()
|
39
39
|
|
@@ -20,9 +20,9 @@ _STREAM_LOCK = threading.Lock()
|
|
20
20
|
def _is_in_click_testing() -> bool:
|
21
21
|
"""Check if we're running inside Click's testing framework."""
|
22
22
|
import inspect
|
23
|
-
|
23
|
+
|
24
24
|
config = get_stream_config()
|
25
|
-
|
25
|
+
|
26
26
|
# Check environment variables for Click testing
|
27
27
|
if config.click_testing:
|
28
28
|
return True
|
@@ -21,7 +21,7 @@ from provide.foundation.utils.streams import get_safe_stderr
|
|
21
21
|
def _safe_error_output(message: str) -> None:
|
22
22
|
"""
|
23
23
|
Output error message to stderr using basic print to avoid circular dependencies.
|
24
|
-
|
24
|
+
|
25
25
|
This function intentionally uses print() instead of Foundation's perr() to prevent
|
26
26
|
circular import issues during stream initialization and teardown phases.
|
27
27
|
"""
|
@@ -98,12 +98,22 @@ def __getattr__(name: str) -> Any:
|
|
98
98
|
import provide.foundation.testing.fixtures as fixtures_module
|
99
99
|
|
100
100
|
return getattr(fixtures_module, name)
|
101
|
-
|
101
|
+
|
102
102
|
# Import submodules directly
|
103
|
-
elif name in [
|
103
|
+
elif name in [
|
104
|
+
"archive",
|
105
|
+
"common",
|
106
|
+
"file",
|
107
|
+
"process",
|
108
|
+
"transport",
|
109
|
+
"mocking",
|
110
|
+
"time",
|
111
|
+
"threading",
|
112
|
+
]:
|
104
113
|
import importlib
|
114
|
+
|
105
115
|
return importlib.import_module(f"provide.foundation.testing.{name}")
|
106
|
-
|
116
|
+
|
107
117
|
# File testing utilities (backward compatibility)
|
108
118
|
elif name in [
|
109
119
|
"temp_directory",
|
@@ -115,8 +125,9 @@ def __getattr__(name: str) -> Any:
|
|
115
125
|
"readonly_file",
|
116
126
|
]:
|
117
127
|
import provide.foundation.testing.file.fixtures as file_module
|
128
|
+
|
118
129
|
return getattr(file_module, name)
|
119
|
-
|
130
|
+
|
120
131
|
# Process/async testing utilities (backward compatibility)
|
121
132
|
elif name in [
|
122
133
|
"clean_event_loop",
|
@@ -131,8 +142,9 @@ def __getattr__(name: str) -> Any:
|
|
131
142
|
"mock_async_sleep",
|
132
143
|
]:
|
133
144
|
import provide.foundation.testing.process.fixtures as process_module
|
145
|
+
|
134
146
|
return getattr(process_module, name)
|
135
|
-
|
147
|
+
|
136
148
|
# Common mock utilities (backward compatibility)
|
137
149
|
elif name in [
|
138
150
|
"mock_http_config",
|
@@ -147,8 +159,9 @@ def __getattr__(name: str) -> Any:
|
|
147
159
|
"mock_subprocess",
|
148
160
|
]:
|
149
161
|
import provide.foundation.testing.common.fixtures as common_module
|
162
|
+
|
150
163
|
return getattr(common_module, name)
|
151
|
-
|
164
|
+
|
152
165
|
# Transport/network testing utilities (backward compatibility)
|
153
166
|
elif name in [
|
154
167
|
"free_port",
|
@@ -162,8 +175,9 @@ def __getattr__(name: str) -> Any:
|
|
162
175
|
"mock_http_headers",
|
163
176
|
]:
|
164
177
|
import provide.foundation.testing.transport.fixtures as transport_module
|
178
|
+
|
165
179
|
return getattr(transport_module, name)
|
166
|
-
|
180
|
+
|
167
181
|
# Archive testing utilities
|
168
182
|
elif name in [
|
169
183
|
"archive_test_content",
|
@@ -174,6 +188,7 @@ def __getattr__(name: str) -> Any:
|
|
174
188
|
"archive_stress_test_files",
|
175
189
|
]:
|
176
190
|
import provide.foundation.testing.archive.fixtures as archive_module
|
191
|
+
|
177
192
|
return getattr(archive_module, name)
|
178
193
|
|
179
194
|
# Crypto fixtures (many fixtures)
|
@@ -6,19 +6,19 @@ across any project that depends on provide.foundation.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from provide.foundation.testing.archive.fixtures import (
|
9
|
+
archive_stress_test_files,
|
9
10
|
archive_test_content,
|
10
|
-
large_file_for_compression,
|
11
|
-
multi_format_archives,
|
12
11
|
archive_with_permissions,
|
13
12
|
corrupted_archives,
|
14
|
-
|
13
|
+
large_file_for_compression,
|
14
|
+
multi_format_archives,
|
15
15
|
)
|
16
16
|
|
17
17
|
__all__ = [
|
18
|
+
"archive_stress_test_files",
|
18
19
|
"archive_test_content",
|
19
|
-
"large_file_for_compression",
|
20
|
-
"multi_format_archives",
|
21
20
|
"archive_with_permissions",
|
22
21
|
"corrupted_archives",
|
23
|
-
"
|
24
|
-
|
22
|
+
"large_file_for_compression",
|
23
|
+
"multi_format_archives",
|
24
|
+
]
|