provide-foundation 0.0.0.dev0__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 (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,360 @@
1
+ """Error handling utilities and context managers.
2
+
3
+ Provides tools for handling errors in a consistent and structured way.
4
+ """
5
+
6
+ from collections.abc import Callable, Generator
7
+ from contextlib import contextmanager
8
+ from typing import Any, TypeVar
9
+
10
+ from attrs import define, field
11
+
12
+ from provide.foundation.errors.base import FoundationError
13
+ from provide.foundation.errors.context import capture_error_context
14
+
15
+
16
+ def _get_logger():
17
+ """Get logger instance lazily to avoid circular imports."""
18
+ from provide.foundation.logger import logger
19
+
20
+ return logger
21
+
22
+
23
+ T = TypeVar("T")
24
+
25
+
26
+ @contextmanager
27
+ def error_boundary(
28
+ *catch: type[Exception],
29
+ on_error: Callable[[Exception], Any] | None = None,
30
+ log_errors: bool = True,
31
+ reraise: bool = True,
32
+ context: dict[str, Any] | None = None,
33
+ fallback: Any = None,
34
+ ) -> Generator[None, None, None]:
35
+ """Context manager for structured error handling with logging.
36
+
37
+ Args:
38
+ *catch: Exception types to catch (defaults to Exception if empty).
39
+ on_error: Optional callback function when error is caught.
40
+ log_errors: Whether to log caught errors.
41
+ reraise: Whether to re-raise after handling.
42
+ context: Additional context for error logging.
43
+ fallback: Value to return if error is suppressed (when reraise=False).
44
+
45
+ Yields:
46
+ None
47
+
48
+ Examples:
49
+ >>> with error_boundary(ValueError, on_error=handle_error):
50
+ ... risky_operation()
51
+
52
+ >>> # Suppress and log specific errors
53
+ >>> with error_boundary(KeyError, reraise=False, fallback=None):
54
+ ... value = data["missing_key"]
55
+ """
56
+ # Default to catching all exceptions if none specified
57
+ catch_types = catch if catch else (Exception,)
58
+
59
+ try:
60
+ yield
61
+ except catch_types as e:
62
+ if log_errors:
63
+ # Build error context
64
+ error_context = context or {}
65
+
66
+ # Add error details
67
+ error_context.update(
68
+ {
69
+ "error.type": type(e).__name__,
70
+ "error.message": str(e),
71
+ }
72
+ )
73
+
74
+ # If it's a FoundationError, merge its context
75
+ if isinstance(e, FoundationError) and e.context:
76
+ error_context.update(e.context)
77
+
78
+ # Log the error
79
+ _get_logger().error(
80
+ f"Error caught in boundary: {e}", exc_info=True, **error_context
81
+ )
82
+
83
+ # Call error handler if provided
84
+ if on_error:
85
+ try:
86
+ on_error(e)
87
+ except Exception as handler_error:
88
+ if log_errors:
89
+ _get_logger().error(
90
+ f"Error handler failed: {handler_error}",
91
+ exc_info=True,
92
+ original_error=str(e),
93
+ )
94
+
95
+ # Re-raise if configured
96
+ if reraise:
97
+ raise
98
+
99
+ # Return fallback value if not re-raising
100
+ return fallback
101
+
102
+
103
+ @contextmanager
104
+ def transactional(
105
+ rollback: Callable[[], None],
106
+ commit: Callable[[], None] | None = None,
107
+ on_error: Callable[[Exception], None] | None = None,
108
+ log_errors: bool = True,
109
+ ) -> Generator[None, None, None]:
110
+ """Context manager for transactional operations with rollback.
111
+
112
+ Args:
113
+ rollback: Function to call on error to rollback changes.
114
+ commit: Optional function to call on success.
115
+ on_error: Optional error handler before rollback.
116
+ log_errors: Whether to log errors.
117
+
118
+ Yields:
119
+ None
120
+
121
+ Examples:
122
+ >>> def rollback_changes():
123
+ ... db.rollback()
124
+ ...
125
+ >>> def commit_changes():
126
+ ... db.commit()
127
+ ...
128
+ >>> with transactional(rollback_changes, commit_changes):
129
+ ... db.execute("INSERT INTO users ...")
130
+ ... db.execute("UPDATE accounts ...")
131
+ """
132
+ try:
133
+ yield
134
+ # Call commit if provided and no exception occurred
135
+ if commit:
136
+ commit()
137
+ except Exception as e:
138
+ if log_errors:
139
+ _get_logger().error(
140
+ "Transaction failed, rolling back", exc_info=True, error=str(e)
141
+ )
142
+
143
+ # Call error handler if provided
144
+ if on_error:
145
+ try:
146
+ on_error(e)
147
+ except Exception as handler_error:
148
+ if log_errors:
149
+ _get_logger().error(
150
+ f"Transaction error handler failed: {handler_error}",
151
+ original_error=str(e),
152
+ )
153
+
154
+ # Perform rollback
155
+ try:
156
+ rollback()
157
+ if log_errors:
158
+ _get_logger().info("Transaction rolled back successfully")
159
+ except Exception as rollback_error:
160
+ if log_errors:
161
+ _get_logger().critical(
162
+ f"Rollback failed: {rollback_error}", original_error=str(e)
163
+ )
164
+ # Re-raise the rollback error as it's more critical
165
+ raise rollback_error from e
166
+
167
+ # Re-raise original exception
168
+ raise
169
+
170
+
171
+ def handle_error(
172
+ error: Exception,
173
+ *,
174
+ log: bool = True,
175
+ capture_context: bool = True,
176
+ reraise: bool = False,
177
+ fallback: Any = None,
178
+ ) -> Any:
179
+ """Handle an error with logging and optional context capture.
180
+
181
+ Args:
182
+ error: The exception to handle.
183
+ log: Whether to log the error.
184
+ capture_context: Whether to capture error context.
185
+ reraise: Whether to re-raise the error after handling.
186
+ fallback: Value to return if not re-raising.
187
+
188
+ Returns:
189
+ The fallback value if not re-raising.
190
+
191
+ Raises:
192
+ The original error if reraise=True.
193
+
194
+ Examples:
195
+ >>> try:
196
+ ... risky_operation()
197
+ ... except ValueError as e:
198
+ ... result = handle_error(e, fallback="default")
199
+ """
200
+ # Capture context if requested
201
+ context = None
202
+ if capture_context:
203
+ context = capture_error_context(error)
204
+
205
+ # Log if requested
206
+ if log:
207
+ log_context = context.to_dict() if context else {}
208
+ _get_logger().error(f"Handling error: {error}", exc_info=True, **log_context)
209
+
210
+ # Re-raise if requested
211
+ if reraise:
212
+ raise error
213
+
214
+ return fallback
215
+
216
+
217
+ @define(kw_only=True, slots=True)
218
+ class ErrorHandler:
219
+ """Configurable error handler with type-based policies.
220
+
221
+ Attributes:
222
+ policies: Mapping of error types to handler functions.
223
+ default_action: Default handler for unmatched errors.
224
+ log_all: Whether to log all handled errors.
225
+ capture_context: Whether to capture error context.
226
+ reraise_unhandled: Whether to re-raise unhandled errors.
227
+
228
+ Examples:
229
+ >>> def handle_validation(e: ValidationError):
230
+ ... return {"error": "Invalid input", "details": e.context}
231
+ ...
232
+ >>> handler = ErrorHandler(
233
+ ... policies={ValidationError: handle_validation},
234
+ ... default_action=lambda e: None
235
+ ... )
236
+ >>> result = handler.handle(some_error)
237
+ """
238
+
239
+ policies: dict[type[Exception], Callable[[Exception], Any]] = field(
240
+ factory=lambda: {}
241
+ )
242
+ default_action: Callable[[Exception], Any] = field(default=lambda e: None)
243
+ log_all: bool = True
244
+ capture_context: bool = True
245
+ reraise_unhandled: bool = False
246
+
247
+ def add_policy(
248
+ self, error_type: type[Exception], handler: Callable[[Exception], Any]
249
+ ) -> "ErrorHandler":
250
+ """Add or update a handler policy for an error type.
251
+
252
+ Args:
253
+ error_type: Exception type to handle.
254
+ handler: Handler function for this error type.
255
+
256
+ Returns:
257
+ Self for method chaining.
258
+ """
259
+ self.policies[error_type] = handler
260
+ return self
261
+
262
+ def handle(self, error: Exception) -> Any:
263
+ """Handle an error based on configured policies.
264
+
265
+ Args:
266
+ error: The exception to handle.
267
+
268
+ Returns:
269
+ Result from the handler function.
270
+
271
+ Raises:
272
+ The original error if reraise_unhandled=True and no handler matches.
273
+
274
+ Examples:
275
+ >>> result = handler.handle(ValidationError("Invalid"))
276
+ """
277
+ # Find matching handler
278
+ handler = None
279
+ for error_type, policy_handler in self.policies.items():
280
+ if isinstance(error, error_type):
281
+ handler = policy_handler
282
+ break
283
+
284
+ # Use default if no match
285
+ if handler is None:
286
+ handler = self.default_action
287
+
288
+ # Check if we should re-raise unhandled
289
+ if self.reraise_unhandled and handler is self.default_action:
290
+ if self.log_all:
291
+ _get_logger().warning(
292
+ f"No handler for {type(error).__name__}, re-raising",
293
+ error=str(error),
294
+ )
295
+ raise error
296
+
297
+ # Capture context if configured
298
+ context = None
299
+ if self.capture_context:
300
+ context = capture_error_context(error)
301
+
302
+ # Log if configured
303
+ if self.log_all:
304
+ log_context = context.to_dict() if context else {}
305
+ _get_logger().info(
306
+ f"Handling {type(error).__name__} with {handler.__name__}",
307
+ **log_context,
308
+ )
309
+
310
+ # Execute handler
311
+ try:
312
+ return handler(error)
313
+ except Exception as handler_error:
314
+ if self.log_all:
315
+ _get_logger().error(
316
+ f"Error handler failed: {handler_error}",
317
+ exc_info=True,
318
+ original_error=str(error),
319
+ handler=handler.__name__,
320
+ )
321
+ raise handler_error from error
322
+
323
+
324
+ def create_error_handler(**policies: Callable[[Exception], Any]) -> ErrorHandler:
325
+ """Create an error handler with policies from keyword arguments.
326
+
327
+ Args:
328
+ **policies: Error type names mapped to handler functions.
329
+
330
+ Returns:
331
+ Configured ErrorHandler instance.
332
+
333
+ Examples:
334
+ >>> handler = create_error_handler(
335
+ ... ValidationError=lambda e: {"error": str(e)},
336
+ ... NetworkError=lambda e: retry_operation(),
337
+ ... default=lambda e: None
338
+ ... )
339
+ """
340
+ # Extract default if provided
341
+ default = policies.pop("default", lambda e: None)
342
+
343
+ # Import error types
344
+ import provide.foundation.errors as errors_module
345
+
346
+ # Build policies dict
347
+ error_policies = {}
348
+ for error_name, handler_func in policies.items():
349
+ # Try to get the error class from errors module
350
+ error_class = getattr(errors_module, error_name, None)
351
+ if (
352
+ error_class
353
+ and isinstance(error_class, type)
354
+ and issubclass(error_class, Exception)
355
+ ):
356
+ error_policies[error_class] = handler_func
357
+ else:
358
+ _get_logger().warning(f"Unknown error type: {error_name}")
359
+
360
+ return ErrorHandler(policies=error_policies, default_action=default)
@@ -0,0 +1,105 @@
1
+ """Integration and network-related exceptions."""
2
+
3
+ from typing import Any
4
+
5
+ from provide.foundation.errors.base import FoundationError
6
+
7
+
8
+ class IntegrationError(FoundationError):
9
+ """Raised when external service integration fails.
10
+
11
+ Args:
12
+ message: Error message describing the integration failure.
13
+ service: Optional service name that failed.
14
+ endpoint: Optional endpoint that was called.
15
+ status_code: Optional HTTP status code.
16
+ **kwargs: Additional context passed to FoundationError.
17
+
18
+ Examples:
19
+ >>> raise IntegrationError("API call failed")
20
+ >>> raise IntegrationError("Auth failed", service="github", status_code=401)
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ message: str,
26
+ *,
27
+ service: str | None = None,
28
+ endpoint: str | None = None,
29
+ status_code: int | None = None,
30
+ **kwargs: Any,
31
+ ) -> None:
32
+ if service:
33
+ kwargs.setdefault("context", {})["integration.service"] = service
34
+ if endpoint:
35
+ kwargs.setdefault("context", {})["integration.endpoint"] = endpoint
36
+ if status_code:
37
+ kwargs.setdefault("context", {})["integration.status_code"] = status_code
38
+ super().__init__(message, **kwargs)
39
+
40
+ def _default_code(self) -> str:
41
+ return "INTEGRATION_ERROR"
42
+
43
+
44
+ class NetworkError(IntegrationError):
45
+ """Raised for network-related failures.
46
+
47
+ Args:
48
+ message: Error message describing the network issue.
49
+ host: Optional hostname or IP address.
50
+ port: Optional port number.
51
+ **kwargs: Additional context passed to IntegrationError.
52
+
53
+ Examples:
54
+ >>> raise NetworkError("Connection refused")
55
+ >>> raise NetworkError("DNS resolution failed", host="api.example.com")
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ message: str,
61
+ *,
62
+ host: str | None = None,
63
+ port: int | None = None,
64
+ **kwargs: Any,
65
+ ) -> None:
66
+ if host:
67
+ kwargs.setdefault("context", {})["network.host"] = host
68
+ if port:
69
+ kwargs.setdefault("context", {})["network.port"] = port
70
+ super().__init__(message, **kwargs)
71
+
72
+ def _default_code(self) -> str:
73
+ return "NETWORK_ERROR"
74
+
75
+
76
+ class TimeoutError(IntegrationError):
77
+ """Raised when operations exceed time limits.
78
+
79
+ Args:
80
+ message: Error message describing the timeout.
81
+ timeout_seconds: Optional timeout limit in seconds.
82
+ elapsed_seconds: Optional actual elapsed time.
83
+ **kwargs: Additional context passed to IntegrationError.
84
+
85
+ Examples:
86
+ >>> raise TimeoutError("Request timed out")
87
+ >>> raise TimeoutError("Operation exceeded limit", timeout_seconds=30, elapsed_seconds=31.5)
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ message: str,
93
+ *,
94
+ timeout_seconds: float | None = None,
95
+ elapsed_seconds: float | None = None,
96
+ **kwargs: Any,
97
+ ) -> None:
98
+ if timeout_seconds is not None:
99
+ kwargs.setdefault("context", {})["timeout.limit"] = timeout_seconds
100
+ if elapsed_seconds is not None:
101
+ kwargs.setdefault("context", {})["timeout.elapsed"] = elapsed_seconds
102
+ super().__init__(message, **kwargs)
103
+
104
+ def _default_code(self) -> str:
105
+ return "TIMEOUT_ERROR"
@@ -0,0 +1,37 @@
1
+ """Platform detection and system-related exceptions."""
2
+
3
+ from typing import Any
4
+
5
+ from provide.foundation.errors.base import FoundationError
6
+
7
+
8
+ class PlatformError(FoundationError):
9
+ """Raised when platform detection or system operations fail.
10
+
11
+ Args:
12
+ message: Error message describing the platform issue.
13
+ platform: Optional platform identifier.
14
+ operation: Optional operation that failed.
15
+ **kwargs: Additional context passed to FoundationError.
16
+
17
+ Examples:
18
+ >>> raise PlatformError("Failed to detect OS")
19
+ >>> raise PlatformError("Unsupported platform", platform="freebsd")
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ platform: str | None = None,
27
+ operation: str | None = None,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ if platform:
31
+ kwargs.setdefault("context", {})["platform.name"] = platform
32
+ if operation:
33
+ kwargs.setdefault("context", {})["platform.operation"] = operation
34
+ super().__init__(message, **kwargs)
35
+
36
+ def _default_code(self) -> str:
37
+ return "PLATFORM_ERROR"
@@ -0,0 +1,140 @@
1
+ #
2
+ # provide/foundation/errors/process.py
3
+ #
4
+ """Process execution related errors."""
5
+
6
+ from provide.foundation.errors.base import FoundationError
7
+
8
+
9
+ class ProcessError(FoundationError):
10
+ """Error for external process execution failures with output capture."""
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ *,
16
+ command: str | list[str] | None = None,
17
+ return_code: int | None = None,
18
+ stdout: str | bytes | None = None,
19
+ stderr: str | bytes | None = None,
20
+ timeout: bool = False,
21
+ **extra_context,
22
+ ) -> None:
23
+ """Initialize ProcessError with command execution details.
24
+
25
+ Args:
26
+ message: Human-readable error message
27
+ command: The command that was executed
28
+ return_code: Process return/exit code
29
+ stdout: Standard output from the process
30
+ stderr: Standard error from the process
31
+ timeout: Whether the process timed out
32
+ **extra_context: Additional context information
33
+ """
34
+ # Build comprehensive error message
35
+ full_message = message
36
+
37
+ if command:
38
+ cmd_str = command if isinstance(command, str) else " ".join(command)
39
+ full_message += f"\nCommand: {cmd_str}"
40
+
41
+ if return_code is not None:
42
+ full_message += f"\nReturn code: {return_code}"
43
+
44
+ if timeout:
45
+ full_message += "\nProcess timed out"
46
+
47
+ if stdout:
48
+ stdout_str = (
49
+ stdout.decode("utf-8", "replace")
50
+ if isinstance(stdout, bytes)
51
+ else stdout
52
+ )
53
+ if stdout_str.strip():
54
+ full_message += f"\n--- STDOUT ---\n{stdout_str.strip()}"
55
+
56
+ if stderr:
57
+ stderr_str = (
58
+ stderr.decode("utf-8", "replace")
59
+ if isinstance(stderr, bytes)
60
+ else stderr
61
+ )
62
+ if stderr_str.strip():
63
+ full_message += f"\n--- STDERR ---\n{stderr_str.strip()}"
64
+
65
+ # Store structured data
66
+ context = extra_context.copy()
67
+ context.update(
68
+ {
69
+ "process.command": command,
70
+ "process.return_code": return_code,
71
+ "process.timeout": timeout,
72
+ }
73
+ )
74
+
75
+ # Store clean stdout/stderr for programmatic access
76
+ self.stdout = (
77
+ stdout.decode("utf-8", "replace").strip()
78
+ if isinstance(stdout, bytes)
79
+ else stdout.strip()
80
+ if stdout
81
+ else None
82
+ )
83
+
84
+ self.stderr = (
85
+ stderr.decode("utf-8", "replace").strip()
86
+ if isinstance(stderr, bytes)
87
+ else stderr.strip()
88
+ if stderr
89
+ else None
90
+ )
91
+
92
+ self.command = command
93
+ self.return_code = return_code
94
+ self.timeout = timeout
95
+
96
+ super().__init__(full_message, context=context)
97
+
98
+ def _default_code(self) -> str:
99
+ """Return default error code for process errors."""
100
+ return "PROCESS_ERROR"
101
+
102
+
103
+ class CommandNotFoundError(ProcessError):
104
+ """Error when a command/executable is not found."""
105
+
106
+ def _default_code(self) -> str:
107
+ return "COMMAND_NOT_FOUND"
108
+
109
+
110
+ class ProcessTimeoutError(ProcessError):
111
+ """Error when a process times out."""
112
+
113
+ def __init__(
114
+ self,
115
+ message: str,
116
+ *,
117
+ command: str | list[str] | None = None,
118
+ timeout_seconds: float | None = None,
119
+ stdout: str | bytes | None = None,
120
+ stderr: str | bytes | None = None,
121
+ **extra_context,
122
+ ) -> None:
123
+ context = extra_context.copy()
124
+ if timeout_seconds is not None:
125
+ context["process.timeout_seconds"] = timeout_seconds
126
+
127
+ super().__init__(
128
+ message,
129
+ command=command,
130
+ stdout=stdout,
131
+ stderr=stderr,
132
+ timeout=True,
133
+ **context,
134
+ )
135
+
136
+ def _default_code(self) -> str:
137
+ return "PROCESS_TIMEOUT"
138
+
139
+
140
+ # 🏗️⚡️⚙️🪄