runbooks 0.7.6__py3-none-any.whl → 0.7.9__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 (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +5 -1
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +871 -0
  6. runbooks/cfat/assessment/runner.py +122 -11
  7. runbooks/cfat/models.py +6 -2
  8. runbooks/common/logger.py +14 -0
  9. runbooks/common/rich_utils.py +451 -0
  10. runbooks/enterprise/__init__.py +68 -0
  11. runbooks/enterprise/error_handling.py +411 -0
  12. runbooks/enterprise/logging.py +439 -0
  13. runbooks/enterprise/multi_tenant.py +583 -0
  14. runbooks/finops/README.md +468 -241
  15. runbooks/finops/__init__.py +39 -3
  16. runbooks/finops/cli.py +83 -18
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +812 -164
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +485 -51
  22. runbooks/finops/optimizer.py +823 -0
  23. runbooks/finops/tests/__init__.py +19 -0
  24. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  25. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  26. runbooks/finops/tests/run_tests.py +305 -0
  27. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  28. runbooks/finops/tests/test_integration.py +477 -0
  29. runbooks/finops/tests/test_performance.py +380 -0
  30. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  31. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  32. runbooks/finops/tests/test_single_account_features.py +715 -0
  33. runbooks/finops/tests/validate_test_suite.py +220 -0
  34. runbooks/finops/types.py +1 -1
  35. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  36. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  37. runbooks/inventory/collectors/aws_comprehensive.py +442 -0
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/discovery.md +1 -1
  41. runbooks/inventory/list_ec2_instances.py +18 -20
  42. runbooks/inventory/list_ssm_parameters.py +31 -3
  43. runbooks/inventory/organizations_discovery.py +1269 -0
  44. runbooks/inventory/rich_inventory_display.py +393 -0
  45. runbooks/inventory/run_on_multi_accounts.py +35 -19
  46. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  47. runbooks/inventory/runbooks.security.run_script.log +0 -0
  48. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  49. runbooks/main.py +2215 -119
  50. runbooks/metrics/dora_metrics_engine.py +599 -0
  51. runbooks/operate/__init__.py +2 -2
  52. runbooks/operate/base.py +122 -10
  53. runbooks/operate/deployment_framework.py +1032 -0
  54. runbooks/operate/deployment_validator.py +853 -0
  55. runbooks/operate/dynamodb_operations.py +10 -6
  56. runbooks/operate/ec2_operations.py +319 -11
  57. runbooks/operate/executive_dashboard.py +779 -0
  58. runbooks/operate/mcp_integration.py +750 -0
  59. runbooks/operate/nat_gateway_operations.py +1120 -0
  60. runbooks/operate/networking_cost_heatmap.py +685 -0
  61. runbooks/operate/privatelink_operations.py +940 -0
  62. runbooks/operate/s3_operations.py +10 -6
  63. runbooks/operate/vpc_endpoints.py +644 -0
  64. runbooks/operate/vpc_operations.py +1038 -0
  65. runbooks/remediation/__init__.py +2 -2
  66. runbooks/remediation/acm_remediation.py +1 -1
  67. runbooks/remediation/base.py +1 -1
  68. runbooks/remediation/cloudtrail_remediation.py +1 -1
  69. runbooks/remediation/cognito_remediation.py +1 -1
  70. runbooks/remediation/dynamodb_remediation.py +1 -1
  71. runbooks/remediation/ec2_remediation.py +1 -1
  72. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  73. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  74. runbooks/remediation/kms_remediation.py +1 -1
  75. runbooks/remediation/lambda_remediation.py +1 -1
  76. runbooks/remediation/multi_account.py +1 -1
  77. runbooks/remediation/rds_remediation.py +1 -1
  78. runbooks/remediation/s3_block_public_access.py +1 -1
  79. runbooks/remediation/s3_enable_access_logging.py +1 -1
  80. runbooks/remediation/s3_encryption.py +1 -1
  81. runbooks/remediation/s3_remediation.py +1 -1
  82. runbooks/remediation/vpc_remediation.py +475 -0
  83. runbooks/security/__init__.py +3 -1
  84. runbooks/security/compliance_automation.py +632 -0
  85. runbooks/security/report_generator.py +10 -0
  86. runbooks/security/run_script.py +31 -5
  87. runbooks/security/security_baseline_tester.py +169 -30
  88. runbooks/security/security_export.py +477 -0
  89. runbooks/validation/__init__.py +10 -0
  90. runbooks/validation/benchmark.py +484 -0
  91. runbooks/validation/cli.py +356 -0
  92. runbooks/validation/mcp_validator.py +768 -0
  93. runbooks/vpc/__init__.py +38 -0
  94. runbooks/vpc/config.py +212 -0
  95. runbooks/vpc/cost_engine.py +347 -0
  96. runbooks/vpc/heatmap_engine.py +605 -0
  97. runbooks/vpc/manager_interface.py +634 -0
  98. runbooks/vpc/networking_wrapper.py +1260 -0
  99. runbooks/vpc/rich_formatters.py +679 -0
  100. runbooks/vpc/tests/__init__.py +5 -0
  101. runbooks/vpc/tests/conftest.py +356 -0
  102. runbooks/vpc/tests/test_cli_integration.py +530 -0
  103. runbooks/vpc/tests/test_config.py +458 -0
  104. runbooks/vpc/tests/test_cost_engine.py +479 -0
  105. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  106. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
  107. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
  108. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
  109. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,68 @@
1
+ """
2
+ Enterprise enhancements for CloudOps-Runbooks.
3
+
4
+ This module provides enterprise-grade enhancements including:
5
+ - Advanced error handling with actionable guidance
6
+ - Structured logging for enterprise monitoring
7
+ - Security hardening and compliance validation
8
+ - Enhanced configuration management
9
+ - Professional documentation standards
10
+ """
11
+
12
+ from .error_handling import (
13
+ AWSServiceError,
14
+ ConfigurationError,
15
+ EnterpriseErrorHandler,
16
+ RunbooksException,
17
+ SecurityError,
18
+ ValidationError,
19
+ create_user_friendly_error,
20
+ )
21
+ from .logging import (
22
+ AuditLogger,
23
+ EnterpriseLogger,
24
+ PerformanceLogger,
25
+ configure_enterprise_logging,
26
+ )
27
+ from .security import (
28
+ ComplianceChecker,
29
+ SecurityValidator,
30
+ ZeroTrustValidator,
31
+ sanitize_input,
32
+ validate_aws_permissions,
33
+ )
34
+ from .validation import (
35
+ ConfigValidator,
36
+ InputValidator,
37
+ TypeValidator,
38
+ validate_configuration,
39
+ validate_user_input,
40
+ )
41
+
42
+ __all__ = [
43
+ # Error handling
44
+ "EnterpriseErrorHandler",
45
+ "RunbooksException",
46
+ "ConfigurationError",
47
+ "ValidationError",
48
+ "SecurityError",
49
+ "AWSServiceError",
50
+ "create_user_friendly_error",
51
+ # Logging
52
+ "EnterpriseLogger",
53
+ "AuditLogger",
54
+ "PerformanceLogger",
55
+ "configure_enterprise_logging",
56
+ # Security
57
+ "SecurityValidator",
58
+ "ComplianceChecker",
59
+ "ZeroTrustValidator",
60
+ "sanitize_input",
61
+ "validate_aws_permissions",
62
+ # Validation
63
+ "ConfigValidator",
64
+ "InputValidator",
65
+ "TypeValidator",
66
+ "validate_configuration",
67
+ "validate_user_input",
68
+ ]
@@ -0,0 +1,411 @@
1
+ """
2
+ Enterprise-grade error handling for CloudOps-Runbooks.
3
+
4
+ This module provides comprehensive error handling with actionable user guidance,
5
+ structured error logging, and user-friendly error messages that help users
6
+ resolve issues quickly.
7
+ """
8
+
9
+ import sys
10
+ import traceback
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional, Type, Union
14
+
15
+ import click
16
+ from loguru import logger
17
+
18
+ try:
19
+ from rich.console import Console
20
+ from rich.panel import Panel
21
+ from rich.table import Table
22
+ from rich.text import Text
23
+
24
+ _HAS_RICH = True
25
+ except ImportError:
26
+ _HAS_RICH = False
27
+
28
+
29
+ class RunbooksException(Exception):
30
+ """Base exception for all CloudOps-Runbooks errors."""
31
+
32
+ def __init__(
33
+ self,
34
+ message: str,
35
+ error_code: str = "RUNBOOKS_ERROR",
36
+ suggestion: Optional[str] = None,
37
+ documentation_url: Optional[str] = None,
38
+ context: Optional[Dict[str, Any]] = None,
39
+ ):
40
+ """
41
+ Initialize enterprise exception.
42
+
43
+ Args:
44
+ message: Error message
45
+ error_code: Unique error code for troubleshooting
46
+ suggestion: Actionable suggestion to resolve the issue
47
+ documentation_url: URL to relevant documentation
48
+ context: Additional context for debugging
49
+ """
50
+ super().__init__(message)
51
+ self.error_code = error_code
52
+ self.suggestion = suggestion
53
+ self.documentation_url = documentation_url
54
+ self.context = context or {}
55
+ self.timestamp = datetime.utcnow().isoformat()
56
+
57
+
58
+ class ConfigurationError(RunbooksException):
59
+ """Configuration-related errors."""
60
+
61
+ def __init__(self, message: str, **kwargs):
62
+ super().__init__(
63
+ message,
64
+ error_code="CONFIG_ERROR",
65
+ documentation_url="https://cloudops.oceansoft.io/configuration/",
66
+ **kwargs,
67
+ )
68
+
69
+
70
+ class ValidationError(RunbooksException):
71
+ """Input validation errors."""
72
+
73
+ def __init__(self, message: str, **kwargs):
74
+ super().__init__(
75
+ message,
76
+ error_code="VALIDATION_ERROR",
77
+ documentation_url="https://cloudops.oceansoft.io/validation/",
78
+ **kwargs,
79
+ )
80
+
81
+
82
+ class SecurityError(RunbooksException):
83
+ """Security-related errors."""
84
+
85
+ def __init__(self, message: str, **kwargs):
86
+ super().__init__(
87
+ message, error_code="SECURITY_ERROR", documentation_url="https://cloudops.oceansoft.io/security/", **kwargs
88
+ )
89
+
90
+
91
+ class AWSServiceError(RunbooksException):
92
+ """AWS service operation errors."""
93
+
94
+ def __init__(
95
+ self,
96
+ message: str,
97
+ service: str = "unknown",
98
+ operation: str = "unknown",
99
+ aws_error_code: Optional[str] = None,
100
+ **kwargs,
101
+ ):
102
+ super().__init__(
103
+ message,
104
+ error_code=f"AWS_{service.upper()}_ERROR",
105
+ documentation_url=f"https://cloudops.oceansoft.io/aws/{service}/",
106
+ **kwargs,
107
+ )
108
+ self.service = service
109
+ self.operation = operation
110
+ self.aws_error_code = aws_error_code
111
+
112
+
113
+ class EnterpriseErrorHandler:
114
+ """Enterprise-grade error handler with comprehensive error management."""
115
+
116
+ def __init__(self, console: Optional[Console] = None):
117
+ """Initialize error handler."""
118
+ self.console = console or Console()
119
+ self.error_registry: Dict[str, Dict[str, Any]] = {}
120
+ self._setup_error_registry()
121
+
122
+ def _setup_error_registry(self) -> None:
123
+ """Setup common error patterns and their solutions."""
124
+ self.error_registry = {
125
+ "NoCredentialsError": {
126
+ "title": "AWS Credentials Not Found",
127
+ "suggestion": "Configure AWS credentials using 'aws configure' or set environment variables",
128
+ "commands": ["aws configure", "export AWS_PROFILE=your-profile"],
129
+ "documentation": "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html",
130
+ },
131
+ "UnauthorizedOperation": {
132
+ "title": "AWS Permission Denied",
133
+ "suggestion": "Check your AWS IAM permissions for the required service",
134
+ "commands": ["aws sts get-caller-identity", "runbooks security assess --profile your-profile"],
135
+ "documentation": "https://cloudops.oceansoft.io/security/permissions/",
136
+ },
137
+ "ProfileNotFound": {
138
+ "title": "AWS Profile Not Found",
139
+ "suggestion": "Verify the AWS profile exists in ~/.aws/credentials or ~/.aws/config",
140
+ "commands": ["aws configure list-profiles", "runbooks --profile default"],
141
+ "documentation": "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html",
142
+ },
143
+ "InvalidRegion": {
144
+ "title": "Invalid AWS Region",
145
+ "suggestion": "Use a valid AWS region (e.g., us-east-1, eu-west-1)",
146
+ "commands": ["aws ec2 describe-regions", "runbooks --region us-east-1"],
147
+ "documentation": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html",
148
+ },
149
+ "FileNotFoundError": {
150
+ "title": "Configuration File Not Found",
151
+ "suggestion": "Initialize configuration or check file path",
152
+ "commands": ["runbooks --help", "runbooks cfat assess --create-config"],
153
+ "documentation": "https://cloudops.oceansoft.io/configuration/",
154
+ },
155
+ }
156
+
157
+ def handle_exception(
158
+ self,
159
+ exc: Exception,
160
+ command_name: Optional[str] = None,
161
+ context: Optional[Dict[str, Any]] = None,
162
+ show_traceback: bool = False,
163
+ ) -> None:
164
+ """
165
+ Handle exception with enterprise-grade error reporting.
166
+
167
+ Args:
168
+ exc: Exception to handle
169
+ command_name: Name of command that failed
170
+ context: Additional context information
171
+ show_traceback: Whether to show full traceback
172
+ """
173
+ error_info = self._analyze_exception(exc, context)
174
+
175
+ # Log structured error for monitoring
176
+ logger.error(
177
+ "Enterprise error handler activated",
178
+ error_type=type(exc).__name__,
179
+ error_code=getattr(exc, "error_code", "UNKNOWN"),
180
+ command=command_name,
181
+ context=context,
182
+ error_message=str(exc),
183
+ )
184
+
185
+ # Display user-friendly error
186
+ self._display_user_error(error_info, show_traceback)
187
+
188
+ # Save error details for support
189
+ self._save_error_details(exc, error_info, context)
190
+
191
+ def _analyze_exception(self, exc: Exception, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
192
+ """Analyze exception and determine appropriate response."""
193
+ exc_name = type(exc).__name__
194
+ exc_str = str(exc)
195
+
196
+ # Check for known patterns
197
+ error_pattern = None
198
+ for pattern, info in self.error_registry.items():
199
+ if pattern in exc_name or pattern.lower() in exc_str.lower():
200
+ error_pattern = info
201
+ break
202
+
203
+ # Handle RunbooksException instances
204
+ if isinstance(exc, RunbooksException):
205
+ return {
206
+ "title": exc.error_code.replace("_", " ").title(),
207
+ "message": str(exc),
208
+ "suggestion": exc.suggestion,
209
+ "documentation": exc.documentation_url,
210
+ "context": exc.context,
211
+ "commands": [],
212
+ "error_code": exc.error_code,
213
+ }
214
+
215
+ # Use pattern if found
216
+ if error_pattern:
217
+ return {
218
+ **error_pattern,
219
+ "message": str(exc),
220
+ "context": context or {},
221
+ "error_code": exc_name,
222
+ }
223
+
224
+ # Generic error handling
225
+ return {
226
+ "title": "Unexpected Error",
227
+ "message": str(exc),
228
+ "suggestion": "This appears to be an unexpected error. Please check the logs for details.",
229
+ "commands": ["runbooks --debug", "Check logs in ~/.runbooks/logs/"],
230
+ "documentation": "https://cloudops.oceansoft.io/troubleshooting/",
231
+ "context": context or {},
232
+ "error_code": exc_name,
233
+ }
234
+
235
+ def _display_user_error(self, error_info: Dict[str, Any], show_traceback: bool = False) -> None:
236
+ """Display user-friendly error information."""
237
+ if _HAS_RICH:
238
+ self._display_rich_error(error_info, show_traceback)
239
+ else:
240
+ self._display_plain_error(error_info, show_traceback)
241
+
242
+ def _display_rich_error(self, error_info: Dict[str, Any], show_traceback: bool = False) -> None:
243
+ """Display error using Rich formatting."""
244
+ # Main error panel
245
+ error_text = Text()
246
+ error_text.append("āŒ ", style="red bold")
247
+ error_text.append(error_info["title"], style="red bold")
248
+ error_text.append(f"\n\n{error_info['message']}", style="red")
249
+
250
+ if error_info.get("suggestion"):
251
+ error_text.append(f"\n\nšŸ’” Suggestion: {error_info['suggestion']}", style="yellow")
252
+
253
+ panel = Panel(
254
+ error_text,
255
+ title="CloudOps Runbooks Error",
256
+ title_align="left",
257
+ border_style="red",
258
+ padding=(1, 2),
259
+ )
260
+ self.console.print(panel)
261
+
262
+ # Commands to try
263
+ if error_info.get("commands"):
264
+ self.console.print("\nšŸ”§ Try these commands:", style="cyan bold")
265
+ for cmd in error_info["commands"]:
266
+ self.console.print(f" {cmd}", style="cyan")
267
+
268
+ # Documentation link
269
+ if error_info.get("documentation"):
270
+ self.console.print(f"\nšŸ“– Documentation: {error_info['documentation']}", style="blue underline")
271
+
272
+ # Error code for support
273
+ if error_info.get("error_code"):
274
+ self.console.print(f"\nšŸ·ļø Error Code: {error_info['error_code']}", style="dim")
275
+
276
+ # Traceback if requested
277
+ if show_traceback:
278
+ self.console.print("\nšŸ“‹ Traceback:", style="dim")
279
+ self.console.print(traceback.format_exc(), style="dim")
280
+
281
+ def _display_plain_error(self, error_info: Dict[str, Any], show_traceback: bool = False) -> None:
282
+ """Display error using plain text formatting."""
283
+ print(f"\nāŒ {error_info['title']}")
284
+ print(f"\n{error_info['message']}")
285
+
286
+ if error_info.get("suggestion"):
287
+ print(f"\nšŸ’” Suggestion: {error_info['suggestion']}")
288
+
289
+ if error_info.get("commands"):
290
+ print(f"\nšŸ”§ Try these commands:")
291
+ for cmd in error_info["commands"]:
292
+ print(f" {cmd}")
293
+
294
+ if error_info.get("documentation"):
295
+ print(f"\nšŸ“– Documentation: {error_info['documentation']}")
296
+
297
+ if error_info.get("error_code"):
298
+ print(f"\nšŸ·ļø Error Code: {error_info['error_code']}")
299
+
300
+ if show_traceback:
301
+ print(f"\nšŸ“‹ Traceback:")
302
+ print(traceback.format_exc())
303
+
304
+ def _save_error_details(
305
+ self, exc: Exception, error_info: Dict[str, Any], context: Optional[Dict[str, Any]] = None
306
+ ) -> None:
307
+ """Save error details for support and debugging."""
308
+ try:
309
+ error_log_dir = Path.home() / ".runbooks" / "logs" / "errors"
310
+ error_log_dir.mkdir(parents=True, exist_ok=True)
311
+
312
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
313
+ error_file = error_log_dir / f"error_{timestamp}.json"
314
+
315
+ error_data = {
316
+ "timestamp": datetime.utcnow().isoformat(),
317
+ "error_type": type(exc).__name__,
318
+ "error_message": str(exc),
319
+ "error_info": error_info,
320
+ "context": context,
321
+ "traceback": traceback.format_exc(),
322
+ "system_info": {
323
+ "python_version": sys.version,
324
+ "platform": sys.platform,
325
+ },
326
+ }
327
+
328
+ import json
329
+
330
+ with open(error_file, "w") as f:
331
+ json.dump(error_data, f, indent=2, default=str)
332
+
333
+ logger.debug(f"Error details saved to {error_file}")
334
+
335
+ except Exception as save_error:
336
+ logger.error(f"Failed to save error details: {save_error}")
337
+
338
+
339
+ def create_user_friendly_error(
340
+ message: str,
341
+ error_type: Type[Exception] = RunbooksException,
342
+ suggestion: Optional[str] = None,
343
+ commands: Optional[List[str]] = None,
344
+ **kwargs,
345
+ ) -> Exception:
346
+ """
347
+ Create a user-friendly error with actionable guidance.
348
+
349
+ Args:
350
+ message: Error message
351
+ error_type: Type of exception to create
352
+ suggestion: Suggested solution
353
+ commands: List of commands to try
354
+ **kwargs: Additional arguments for exception
355
+
356
+ Returns:
357
+ Configured exception instance
358
+ """
359
+ if issubclass(error_type, RunbooksException):
360
+ return error_type(message=message, suggestion=suggestion, **kwargs)
361
+ else:
362
+ return error_type(message)
363
+
364
+
365
+ # Global error handler instance
366
+ _global_error_handler: Optional[EnterpriseErrorHandler] = None
367
+
368
+
369
+ def get_error_handler() -> EnterpriseErrorHandler:
370
+ """Get global error handler instance."""
371
+ global _global_error_handler
372
+ if _global_error_handler is None:
373
+ _global_error_handler = EnterpriseErrorHandler()
374
+ return _global_error_handler
375
+
376
+
377
+ def handle_cli_exception(exc: Exception, command_name: Optional[str] = None, debug: bool = False) -> None:
378
+ """
379
+ Handle CLI exception with proper error reporting.
380
+
381
+ Args:
382
+ exc: Exception to handle
383
+ command_name: Name of CLI command that failed
384
+ debug: Whether to show debug information
385
+ """
386
+ handler = get_error_handler()
387
+ handler.handle_exception(
388
+ exc,
389
+ command_name=command_name,
390
+ show_traceback=debug,
391
+ )
392
+
393
+
394
+ def enterprise_exception_handler(func):
395
+ """Decorator for enterprise exception handling."""
396
+ from functools import wraps
397
+
398
+ @wraps(func)
399
+ def wrapper(*args, **kwargs):
400
+ try:
401
+ return func(*args, **kwargs)
402
+ except Exception as e:
403
+ handler = get_error_handler()
404
+ handler.handle_exception(
405
+ e,
406
+ command_name=func.__name__,
407
+ context={"args": str(args), "kwargs": str(kwargs)},
408
+ )
409
+ raise
410
+
411
+ return wrapper