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,81 @@
1
+ """Base exception class for Foundation."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class FoundationError(Exception):
7
+ """Base exception for all Foundation errors.
8
+
9
+ Args:
10
+ message: Human-readable error message.
11
+ code: Optional error code for programmatic handling.
12
+ context: Optional context dictionary with diagnostic data.
13
+ cause: Optional underlying exception that caused this error.
14
+ **extra_context: Additional key-value pairs added to context.
15
+
16
+ Examples:
17
+ >>> raise FoundationError("Operation failed")
18
+ >>> raise FoundationError("Operation failed", code="OP_001")
19
+ >>> raise FoundationError("Operation failed", user_id=123, retry_count=3)
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ code: str | None = None,
27
+ context: dict[str, Any] | None = None,
28
+ cause: Exception | None = None,
29
+ **extra_context: Any,
30
+ ) -> None:
31
+ self.message = message
32
+ self.code = code or self._default_code()
33
+ self.context = context or {}
34
+ self.context.update(extra_context)
35
+ self.cause = cause
36
+ if cause:
37
+ self.__cause__ = cause
38
+ super().__init__(message)
39
+
40
+ def _default_code(self) -> str:
41
+ """Return default error code for this exception type."""
42
+ return "PROVIDE_ERROR"
43
+
44
+ def add_context(self, key: str, value: Any) -> "FoundationError":
45
+ """Add context data to the error.
46
+
47
+ Args:
48
+ key: Context key (use dots for namespacing, e.g., 'aws.region').
49
+ value: Context value.
50
+
51
+ Returns:
52
+ Self for method chaining.
53
+ """
54
+ self.context[key] = value
55
+ return self
56
+
57
+ def to_dict(self) -> dict[str, Any]:
58
+ """Convert exception to dictionary for structured logging.
59
+
60
+ Returns:
61
+ Dictionary representation suitable for logging/serialization.
62
+ """
63
+ result = {
64
+ "error.type": self.__class__.__name__,
65
+ "error.message": self.message,
66
+ "error.code": self.code,
67
+ }
68
+
69
+ # Add context with error prefix
70
+ for key, value in self.context.items():
71
+ # If key already has a prefix, use it; otherwise add error prefix
72
+ if "." in key:
73
+ result[key] = value
74
+ else:
75
+ result[f"error.{key}"] = value
76
+
77
+ if self.cause:
78
+ result["error.cause"] = str(self.cause)
79
+ result["error.cause_type"] = type(self.cause).__name__
80
+
81
+ return result
@@ -0,0 +1,103 @@
1
+ """Configuration-related exceptions."""
2
+
3
+ from typing import Any
4
+
5
+ from provide.foundation.errors.base import FoundationError
6
+
7
+
8
+ class ConfigurationError(FoundationError):
9
+ """Raised when configuration is invalid or cannot be loaded.
10
+
11
+ Args:
12
+ message: Error message describing the configuration issue.
13
+ config_key: Optional configuration key that caused the error.
14
+ config_source: Optional source of the configuration (file, env, etc.).
15
+ **kwargs: Additional context passed to FoundationError.
16
+
17
+ Examples:
18
+ >>> raise ConfigurationError("Missing required config")
19
+ >>> raise ConfigurationError("Invalid timeout", config_key="timeout")
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ message: str,
25
+ *,
26
+ config_key: str | None = None,
27
+ config_source: str | None = None,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ if config_key:
31
+ kwargs.setdefault("context", {})["config.key"] = config_key
32
+ if config_source:
33
+ kwargs.setdefault("context", {})["config.source"] = config_source
34
+ super().__init__(message, **kwargs)
35
+
36
+ def _default_code(self) -> str:
37
+ return "CONFIG_ERROR"
38
+
39
+
40
+ class ValidationError(FoundationError):
41
+ """Raised when data validation fails.
42
+
43
+ Args:
44
+ message: Validation error message.
45
+ field: Optional field name that failed validation.
46
+ value: Optional invalid value.
47
+ rule: Optional validation rule that failed.
48
+ **kwargs: Additional context passed to FoundationError.
49
+
50
+ Examples:
51
+ >>> raise ValidationError("Invalid email format")
52
+ >>> raise ValidationError("Value out of range", field="age", value=-1)
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ message: str,
58
+ *,
59
+ field: str | None = None,
60
+ value: Any = None,
61
+ rule: str | None = None,
62
+ **kwargs: Any,
63
+ ) -> None:
64
+ if field:
65
+ kwargs.setdefault("context", {})["validation.field"] = field
66
+ if value is not None:
67
+ kwargs.setdefault("context", {})["validation.value"] = str(value)
68
+ if rule:
69
+ kwargs.setdefault("context", {})["validation.rule"] = rule
70
+ super().__init__(message, **kwargs)
71
+
72
+ def _default_code(self) -> str:
73
+ return "VALIDATION_ERROR"
74
+
75
+
76
+ class ConfigValidationError(ValidationError):
77
+ """Raised when configuration validation fails.
78
+
79
+ This is a specialized validation error for configuration-specific validation failures.
80
+
81
+ Args:
82
+ message: Validation error message.
83
+ config_class: Optional name of the config class.
84
+ **kwargs: Additional context passed to ValidationError.
85
+
86
+ Examples:
87
+ >>> raise ConfigValidationError("Invalid database configuration")
88
+ >>> raise ConfigValidationError("Port must be positive", field="port", value=-1)
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ message: str,
94
+ *,
95
+ config_class: str | None = None,
96
+ **kwargs: Any,
97
+ ) -> None:
98
+ if config_class:
99
+ kwargs.setdefault("context", {})["config.class"] = config_class
100
+ super().__init__(message, **kwargs)
101
+
102
+ def _default_code(self) -> str:
103
+ return "CONFIG_VALIDATION_ERROR"
@@ -0,0 +1,299 @@
1
+ """Error context management for rich diagnostic information.
2
+
3
+ Provides structured context for errors that can be used for logging,
4
+ monitoring, and diagnostic purposes.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+ from attrs import define, field
12
+
13
+
14
+ class ErrorSeverity(str, Enum):
15
+ """Error severity levels for prioritization and alerting."""
16
+
17
+ LOW = "low"
18
+ MEDIUM = "medium"
19
+ HIGH = "high"
20
+ CRITICAL = "critical"
21
+
22
+
23
+ class ErrorCategory(str, Enum):
24
+ """Error categorization for routing and handling."""
25
+
26
+ USER = "user" # User error (bad input, etc.)
27
+ SYSTEM = "system" # System/infrastructure error
28
+ EXTERNAL = "external" # External service error
29
+
30
+
31
+ @define(kw_only=True, slots=True)
32
+ class ErrorContext:
33
+ """Rich error context for diagnostics and monitoring.
34
+
35
+ Provides a flexible container for error metadata that can be used
36
+ by different systems (logging, monitoring, Terraform, etc.).
37
+
38
+ Attributes:
39
+ timestamp: When the error occurred.
40
+ severity: Error severity level.
41
+ category: Error category for classification.
42
+ metadata: Namespace-based metadata storage.
43
+ tags: Set of tags for categorization and filtering.
44
+ trace_id: Optional trace ID for distributed tracing.
45
+ span_id: Optional span ID for distributed tracing.
46
+
47
+ Examples:
48
+ >>> ctx = ErrorContext(severity=ErrorSeverity.HIGH)
49
+ >>> ctx.add_namespace("aws", {"region": "us-east-1", "account": "123456"})
50
+ >>> ctx.add_tag("production")
51
+ """
52
+
53
+ timestamp: datetime = field(factory=datetime.now)
54
+ severity: ErrorSeverity = ErrorSeverity.MEDIUM
55
+ category: ErrorCategory = ErrorCategory.SYSTEM
56
+ metadata: dict[str, dict[str, Any]] = field(factory=lambda: {})
57
+ tags: set[str] = field(factory=set)
58
+ trace_id: str | None = None
59
+ span_id: str | None = None
60
+
61
+ def add_namespace(self, namespace: str, data: dict[str, Any]) -> "ErrorContext":
62
+ """Add namespaced metadata.
63
+
64
+ Args:
65
+ namespace: Namespace key (e.g., 'terraform', 'aws', 'http').
66
+ data: Metadata for this namespace.
67
+
68
+ Returns:
69
+ Self for method chaining.
70
+
71
+ Examples:
72
+ >>> ctx.add_namespace("terraform", {"provider": "aws", "version": "5.0"})
73
+ >>> ctx.add_namespace("http", {"method": "POST", "status": 500})
74
+ """
75
+ self.metadata[namespace] = data
76
+ return self
77
+
78
+ def update_namespace(self, namespace: str, data: dict[str, Any]) -> "ErrorContext":
79
+ """Update existing namespace metadata.
80
+
81
+ Args:
82
+ namespace: Namespace key to update.
83
+ data: Metadata to merge into namespace.
84
+
85
+ Returns:
86
+ Self for method chaining.
87
+ """
88
+ if namespace not in self.metadata:
89
+ self.metadata[namespace] = {}
90
+ self.metadata[namespace].update(data)
91
+ return self
92
+
93
+ def get_namespace(self, namespace: str) -> dict[str, Any] | None:
94
+ """Get metadata for a specific namespace.
95
+
96
+ Args:
97
+ namespace: Namespace key to retrieve.
98
+
99
+ Returns:
100
+ Namespace metadata or None if not found.
101
+ """
102
+ return self.metadata.get(namespace)
103
+
104
+ def add_tag(self, tag: str) -> "ErrorContext":
105
+ """Add a tag for categorization.
106
+
107
+ Args:
108
+ tag: Tag to add.
109
+
110
+ Returns:
111
+ Self for method chaining.
112
+ """
113
+ self.tags.add(tag)
114
+ return self
115
+
116
+ def add_tags(self, *tags: str) -> "ErrorContext":
117
+ """Add multiple tags.
118
+
119
+ Args:
120
+ *tags: Tags to add.
121
+
122
+ Returns:
123
+ Self for method chaining.
124
+ """
125
+ self.tags.update(tags)
126
+ return self
127
+
128
+ def to_dict(self) -> dict[str, Any]:
129
+ """Convert to dictionary for logging and serialization.
130
+
131
+ Returns:
132
+ Flattened dictionary with namespaced keys.
133
+
134
+ Examples:
135
+ >>> ctx.to_dict()
136
+ {'timestamp': '2024-01-01T12:00:00', 'severity': 'high', ...}
137
+ """
138
+ result: dict[str, Any] = {
139
+ "timestamp": self.timestamp.isoformat(),
140
+ "severity": self.severity.value,
141
+ "category": self.category.value,
142
+ }
143
+
144
+ # Add tracing if present
145
+ if self.trace_id:
146
+ result["trace_id"] = self.trace_id
147
+ if self.span_id:
148
+ result["span_id"] = self.span_id
149
+
150
+ # Flatten namespaced metadata
151
+ for namespace, data in self.metadata.items():
152
+ for key, value in data.items():
153
+ result[f"{namespace}.{key}"] = value
154
+
155
+ # Add tags if present
156
+ if self.tags:
157
+ result["tags"] = sorted(list(self.tags))
158
+
159
+ return result
160
+
161
+ def to_logging_context(self) -> dict[str, Any]:
162
+ """Convert to context suitable for structured logging.
163
+
164
+ Returns:
165
+ Dictionary formatted for logger context.
166
+ """
167
+ return self.to_dict()
168
+
169
+ def to_terraform_diagnostic(self) -> dict[str, Any]:
170
+ """Convert to Terraform diagnostic format.
171
+
172
+ Returns:
173
+ Dictionary formatted for Terraform diagnostics.
174
+
175
+ Examples:
176
+ >>> ctx.to_terraform_diagnostic()
177
+ {'severity': 'error', 'summary': '...', 'detail': {...}}
178
+ """
179
+ # Map our severity to Terraform severity
180
+ severity_map = {
181
+ ErrorSeverity.LOW: "warning",
182
+ ErrorSeverity.MEDIUM: "warning",
183
+ ErrorSeverity.HIGH: "error",
184
+ ErrorSeverity.CRITICAL: "error",
185
+ }
186
+
187
+ diagnostic: dict[str, Any] = {
188
+ "severity": severity_map[self.severity],
189
+ "detail": {},
190
+ }
191
+
192
+ # Add Terraform-specific metadata if present
193
+ tf_meta = self.get_namespace("terraform")
194
+ if tf_meta:
195
+ diagnostic["detail"].update(tf_meta)
196
+
197
+ # Add other relevant metadata
198
+ for namespace, data in self.metadata.items():
199
+ if namespace != "terraform":
200
+ diagnostic["detail"][namespace] = data
201
+
202
+ return diagnostic
203
+
204
+
205
+ def capture_error_context(
206
+ error: Exception,
207
+ severity: ErrorSeverity | None = None,
208
+ category: ErrorCategory | None = None,
209
+ **namespaces: dict[str, Any],
210
+ ) -> ErrorContext:
211
+ """Capture error context from an exception.
212
+
213
+ Args:
214
+ error: Exception to capture context from.
215
+ severity: Optional severity override.
216
+ category: Optional category override.
217
+ **namespaces: Namespace data to add to context.
218
+
219
+ Returns:
220
+ ErrorContext with captured information.
221
+
222
+ Examples:
223
+ >>> try:
224
+ ... risky_operation()
225
+ ... except Exception as e:
226
+ ... ctx = capture_error_context(
227
+ ... e,
228
+ ... severity=ErrorSeverity.HIGH,
229
+ ... aws={"region": "us-east-1"},
230
+ ... http={"status": 500}
231
+ ... )
232
+ """
233
+ # Determine severity based on error type if not provided
234
+ if severity is None:
235
+ if isinstance(error, AssertionError | ValueError | TypeError):
236
+ severity = ErrorSeverity.MEDIUM
237
+ elif isinstance(error, KeyError | IndexError | AttributeError):
238
+ severity = ErrorSeverity.LOW
239
+ else:
240
+ severity = ErrorSeverity.HIGH
241
+
242
+ # Determine category based on error type if not provided
243
+ if category is None:
244
+ # Import here to avoid circular dependency
245
+ from provide.foundation.errors.auth import (
246
+ AuthenticationError,
247
+ AuthorizationError,
248
+ )
249
+ from provide.foundation.errors.config import ValidationError
250
+ from provide.foundation.errors.integration import IntegrationError, NetworkError
251
+
252
+ if isinstance(
253
+ error, ValidationError | AuthenticationError | AuthorizationError
254
+ ):
255
+ category = ErrorCategory.USER
256
+ elif isinstance(error, IntegrationError | NetworkError):
257
+ category = ErrorCategory.EXTERNAL
258
+ else:
259
+ category = ErrorCategory.SYSTEM
260
+
261
+ # Create context
262
+ ctx = ErrorContext(severity=severity, category=category)
263
+
264
+ # Add error details
265
+ ctx.add_namespace(
266
+ "error",
267
+ {
268
+ "type": type(error).__name__,
269
+ "message": str(error),
270
+ },
271
+ )
272
+
273
+ # Add any namespaces provided
274
+ for namespace, data in namespaces.items():
275
+ ctx.add_namespace(namespace, data)
276
+
277
+ # Add context from FoundationError if applicable
278
+ from provide.foundation.errors.base import FoundationError
279
+
280
+ if isinstance(error, FoundationError) and error.context:
281
+ # Group context items by namespace
282
+ grouped: dict[str, dict[str, Any]] = {}
283
+ for key, value in error.context.items():
284
+ if "." in key:
285
+ namespace, subkey = key.split(".", 1)
286
+ if namespace not in grouped:
287
+ grouped[namespace] = {}
288
+ grouped[namespace][subkey] = value
289
+ else:
290
+ # Put non-namespaced items in 'context' namespace
291
+ if "context" not in grouped:
292
+ grouped["context"] = {}
293
+ grouped["context"][key] = value
294
+
295
+ # Add grouped context to error context
296
+ for namespace, data in grouped.items():
297
+ ctx.update_namespace(namespace, data)
298
+
299
+ return ctx