crackerjack 0.37.9__py3-none-any.whl → 0.45.2__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.
- crackerjack/README.md +19 -0
- crackerjack/__init__.py +30 -1
- crackerjack/__main__.py +342 -1263
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +27 -5
- crackerjack/adapters/_output_paths.py +167 -0
- crackerjack/adapters/_qa_adapter_base.py +309 -0
- crackerjack/adapters/_tool_adapter_base.py +706 -0
- crackerjack/adapters/ai/README.md +65 -0
- crackerjack/adapters/ai/__init__.py +5 -0
- crackerjack/adapters/ai/claude.py +853 -0
- crackerjack/adapters/complexity/README.md +53 -0
- crackerjack/adapters/complexity/__init__.py +10 -0
- crackerjack/adapters/complexity/complexipy.py +641 -0
- crackerjack/adapters/dependency/__init__.py +22 -0
- crackerjack/adapters/dependency/pip_audit.py +418 -0
- crackerjack/adapters/format/README.md +72 -0
- crackerjack/adapters/format/__init__.py +11 -0
- crackerjack/adapters/format/mdformat.py +313 -0
- crackerjack/adapters/format/ruff.py +516 -0
- crackerjack/adapters/lint/README.md +47 -0
- crackerjack/adapters/lint/__init__.py +11 -0
- crackerjack/adapters/lint/codespell.py +273 -0
- crackerjack/adapters/lsp/README.md +49 -0
- crackerjack/adapters/lsp/__init__.py +27 -0
- crackerjack/adapters/{rust_tool_manager.py → lsp/_manager.py} +3 -3
- crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
- crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
- crackerjack/adapters/refactor/README.md +59 -0
- crackerjack/adapters/refactor/__init__.py +12 -0
- crackerjack/adapters/refactor/creosote.py +318 -0
- crackerjack/adapters/refactor/refurb.py +406 -0
- crackerjack/adapters/refactor/skylos.py +494 -0
- crackerjack/adapters/sast/README.md +132 -0
- crackerjack/adapters/sast/__init__.py +32 -0
- crackerjack/adapters/sast/_base.py +201 -0
- crackerjack/adapters/sast/bandit.py +423 -0
- crackerjack/adapters/sast/pyscn.py +405 -0
- crackerjack/adapters/sast/semgrep.py +241 -0
- crackerjack/adapters/security/README.md +111 -0
- crackerjack/adapters/security/__init__.py +17 -0
- crackerjack/adapters/security/gitleaks.py +339 -0
- crackerjack/adapters/type/README.md +52 -0
- crackerjack/adapters/type/__init__.py +12 -0
- crackerjack/adapters/type/pyrefly.py +402 -0
- crackerjack/adapters/type/ty.py +402 -0
- crackerjack/adapters/type/zuban.py +522 -0
- crackerjack/adapters/utility/README.md +51 -0
- crackerjack/adapters/utility/__init__.py +10 -0
- crackerjack/adapters/utility/checks.py +884 -0
- crackerjack/agents/README.md +264 -0
- crackerjack/agents/__init__.py +40 -12
- crackerjack/agents/base.py +1 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +49 -53
- crackerjack/agents/dry_agent.py +187 -3
- crackerjack/agents/enhanced_coordinator.py +279 -0
- crackerjack/agents/enhanced_proactive_agent.py +185 -0
- crackerjack/agents/error_middleware.py +53 -0
- crackerjack/agents/formatting_agent.py +6 -8
- crackerjack/agents/helpers/__init__.py +9 -0
- crackerjack/agents/helpers/performance/__init__.py +22 -0
- crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
- crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
- crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
- crackerjack/agents/helpers/refactoring/__init__.py +22 -0
- crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
- crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
- crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
- crackerjack/agents/helpers/test_creation/__init__.py +19 -0
- crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
- crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
- crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
- crackerjack/agents/performance_agent.py +121 -1152
- crackerjack/agents/refactoring_agent.py +156 -655
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +19 -1605
- crackerjack/api.py +5 -7
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +1 -1
- crackerjack/cli/cache_handlers.py +23 -18
- crackerjack/cli/cache_handlers_enhanced.py +1 -4
- crackerjack/cli/facade.py +70 -8
- crackerjack/cli/formatting.py +13 -0
- crackerjack/cli/handlers/__init__.py +85 -0
- crackerjack/cli/handlers/advanced.py +103 -0
- crackerjack/cli/handlers/ai_features.py +62 -0
- crackerjack/cli/handlers/analytics.py +479 -0
- crackerjack/cli/handlers/changelog.py +271 -0
- crackerjack/cli/handlers/config_handlers.py +16 -0
- crackerjack/cli/handlers/coverage.py +84 -0
- crackerjack/cli/handlers/documentation.py +280 -0
- crackerjack/cli/handlers/main_handlers.py +497 -0
- crackerjack/cli/handlers/monitoring.py +371 -0
- crackerjack/cli/handlers.py +249 -49
- crackerjack/cli/interactive.py +8 -5
- crackerjack/cli/options.py +203 -110
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +60 -24
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +256 -0
- crackerjack/config/global_lock_config.py +191 -54
- crackerjack/config/hooks.py +188 -16
- crackerjack/config/loader.py +239 -0
- crackerjack/config/settings.py +141 -0
- crackerjack/config/tool_commands.py +331 -0
- crackerjack/core/README.md +393 -0
- crackerjack/core/async_workflow_orchestrator.py +79 -53
- crackerjack/core/autofix_coordinator.py +22 -9
- crackerjack/core/container.py +10 -9
- crackerjack/core/enhanced_container.py +9 -9
- crackerjack/core/performance.py +1 -1
- crackerjack/core/performance_monitor.py +5 -3
- crackerjack/core/phase_coordinator.py +1018 -634
- crackerjack/core/proactive_workflow.py +3 -3
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +167 -23
- crackerjack/core/session_coordinator.py +187 -382
- crackerjack/core/timeout_manager.py +161 -44
- crackerjack/core/workflow/__init__.py +21 -0
- crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
- crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
- crackerjack/core/workflow/workflow_issue_parser.py +714 -0
- crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
- crackerjack/core/workflow/workflow_security_gates.py +400 -0
- crackerjack/core/workflow_orchestrator.py +1247 -953
- crackerjack/data/README.md +11 -0
- crackerjack/data/__init__.py +8 -0
- crackerjack/data/models.py +79 -0
- crackerjack/data/repository.py +210 -0
- crackerjack/decorators/README.md +180 -0
- crackerjack/decorators/__init__.py +35 -0
- crackerjack/decorators/error_handling.py +649 -0
- crackerjack/decorators/error_handling_decorators.py +334 -0
- crackerjack/decorators/helpers.py +58 -0
- crackerjack/decorators/patterns.py +281 -0
- crackerjack/decorators/utils.py +58 -0
- crackerjack/docs/README.md +11 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/ai_templates.py +1 -1
- crackerjack/documentation/dual_output_generator.py +11 -9
- crackerjack/documentation/reference_generator.py +104 -59
- crackerjack/dynamic_config.py +52 -61
- crackerjack/errors.py +1 -1
- crackerjack/events/README.md +11 -0
- crackerjack/events/__init__.py +16 -0
- crackerjack/events/telemetry.py +175 -0
- crackerjack/events/workflow_bus.py +346 -0
- crackerjack/exceptions/README.md +301 -0
- crackerjack/exceptions/__init__.py +5 -0
- crackerjack/exceptions/config.py +4 -0
- crackerjack/exceptions/tool_execution_error.py +245 -0
- crackerjack/executors/README.md +591 -0
- crackerjack/executors/__init__.py +2 -0
- crackerjack/executors/async_hook_executor.py +539 -77
- crackerjack/executors/cached_hook_executor.py +3 -3
- crackerjack/executors/hook_executor.py +967 -102
- crackerjack/executors/hook_lock_manager.py +31 -22
- crackerjack/executors/individual_hook_executor.py +66 -32
- crackerjack/executors/lsp_aware_hook_executor.py +136 -57
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +23 -7
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +8 -9
- crackerjack/intelligence/README.md +557 -0
- crackerjack/interactive.py +37 -10
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/async_hook_manager.py +41 -57
- crackerjack/managers/hook_manager.py +449 -79
- crackerjack/managers/publish_manager.py +81 -36
- crackerjack/managers/test_command_builder.py +290 -12
- crackerjack/managers/test_executor.py +93 -8
- crackerjack/managers/test_manager.py +1082 -75
- crackerjack/managers/test_progress.py +118 -26
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/cache.py +25 -2
- crackerjack/mcp/client_runner.py +35 -18
- crackerjack/mcp/context.py +9 -9
- crackerjack/mcp/dashboard.py +24 -8
- crackerjack/mcp/enhanced_progress_monitor.py +34 -23
- crackerjack/mcp/file_monitor.py +27 -6
- crackerjack/mcp/progress_components.py +45 -34
- crackerjack/mcp/progress_monitor.py +6 -9
- crackerjack/mcp/rate_limiter.py +11 -7
- crackerjack/mcp/server.py +2 -0
- crackerjack/mcp/server_core.py +187 -55
- crackerjack/mcp/service_watchdog.py +12 -9
- crackerjack/mcp/task_manager.py +2 -2
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +2 -0
- crackerjack/mcp/tools/core_tools.py +75 -52
- crackerjack/mcp/tools/execution_tools.py +87 -31
- crackerjack/mcp/tools/intelligence_tools.py +2 -2
- crackerjack/mcp/tools/proactive_tools.py +1 -1
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +180 -132
- crackerjack/mcp/tools/workflow_executor.py +87 -46
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/app.py +11 -1
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +27 -4
- crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
- crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
- crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
- crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
- crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
- crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
- crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
- crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
- crackerjack/mcp/websocket/monitoring/factory.py +109 -0
- crackerjack/mcp/websocket/monitoring/filters.py +10 -0
- crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
- crackerjack/mcp/websocket/monitoring/models.py +90 -0
- crackerjack/mcp/websocket/monitoring/utils.py +171 -0
- crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
- crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
- crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
- crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
- crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
- crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
- crackerjack/mcp/websocket/monitoring_endpoints.py +16 -2930
- crackerjack/mcp/websocket/server.py +1 -3
- crackerjack/mcp/websocket/websocket_handler.py +107 -6
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +10 -1
- crackerjack/models/config.py +639 -22
- crackerjack/models/config_adapter.py +6 -6
- crackerjack/models/protocols.py +1167 -23
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +19 -3
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/ai_agent_watchdog.py +5 -4
- crackerjack/monitoring/metrics_collector.py +4 -3
- crackerjack/monitoring/regression_prevention.py +4 -3
- crackerjack/monitoring/websocket_server.py +4 -241
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +20 -67
- crackerjack/orchestration/cache/README.md +312 -0
- crackerjack/orchestration/cache/__init__.py +37 -0
- crackerjack/orchestration/cache/memory_cache.py +338 -0
- crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
- crackerjack/orchestration/config.py +297 -0
- crackerjack/orchestration/coverage_improvement.py +13 -6
- crackerjack/orchestration/execution_strategies.py +6 -6
- crackerjack/orchestration/hook_orchestrator.py +1398 -0
- crackerjack/orchestration/strategies/README.md +401 -0
- crackerjack/orchestration/strategies/__init__.py +39 -0
- crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
- crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
- crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
- crackerjack/orchestration/test_progress_streamer.py +1 -1
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/hooks.py +3 -2
- crackerjack/plugins/loader.py +3 -3
- crackerjack/plugins/managers.py +1 -1
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +8 -21
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
- crackerjack/services/ai/embeddings.py +444 -0
- crackerjack/services/ai/intelligent_commit.py +328 -0
- crackerjack/services/ai/predictive_analytics.py +510 -0
- crackerjack/services/api_extractor.py +5 -3
- crackerjack/services/bounded_status_operations.py +45 -5
- crackerjack/services/cache.py +249 -318
- crackerjack/services/changelog_automation.py +7 -3
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +83 -39
- crackerjack/services/config_merge.py +9 -6
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +13 -26
- crackerjack/services/coverage_badge_service.py +6 -4
- crackerjack/services/coverage_ratchet.py +53 -27
- crackerjack/services/debug.py +18 -7
- crackerjack/services/dependency_analyzer.py +4 -4
- crackerjack/services/dependency_monitor.py +13 -13
- crackerjack/services/documentation_generator.py +4 -2
- crackerjack/services/documentation_service.py +62 -33
- crackerjack/services/enhanced_filesystem.py +81 -27
- crackerjack/services/enterprise_optimizer.py +1 -1
- crackerjack/services/error_pattern_analyzer.py +10 -10
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +5 -7
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +80 -109
- crackerjack/services/git.py +99 -5
- crackerjack/services/health_metrics.py +4 -6
- crackerjack/services/heatmap_generator.py +12 -3
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +101 -49
- crackerjack/services/log_manager.py +2 -2
- crackerjack/services/logging.py +120 -68
- crackerjack/services/lsp_client.py +12 -12
- crackerjack/services/memory_optimizer.py +27 -22
- crackerjack/services/monitoring/README.md +30 -0
- crackerjack/services/monitoring/__init__.py +9 -0
- crackerjack/services/monitoring/dependency_monitor.py +678 -0
- crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
- crackerjack/services/monitoring/health_metrics.py +716 -0
- crackerjack/services/monitoring/metrics.py +587 -0
- crackerjack/services/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
- crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
- crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
- crackerjack/services/parallel_executor.py +166 -55
- crackerjack/services/patterns/__init__.py +142 -0
- crackerjack/services/patterns/agents.py +107 -0
- crackerjack/services/patterns/code/__init__.py +15 -0
- crackerjack/services/patterns/code/detection.py +118 -0
- crackerjack/services/patterns/code/imports.py +107 -0
- crackerjack/services/patterns/code/paths.py +159 -0
- crackerjack/services/patterns/code/performance.py +119 -0
- crackerjack/services/patterns/code/replacement.py +36 -0
- crackerjack/services/patterns/core.py +212 -0
- crackerjack/services/patterns/documentation/__init__.py +14 -0
- crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
- crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
- crackerjack/services/patterns/documentation/docstrings.py +89 -0
- crackerjack/services/patterns/formatting.py +226 -0
- crackerjack/services/patterns/operations.py +339 -0
- crackerjack/services/patterns/security/__init__.py +23 -0
- crackerjack/services/patterns/security/code_injection.py +122 -0
- crackerjack/services/patterns/security/credentials.py +190 -0
- crackerjack/services/patterns/security/path_traversal.py +221 -0
- crackerjack/services/patterns/security/unsafe_operations.py +216 -0
- crackerjack/services/patterns/templates.py +62 -0
- crackerjack/services/patterns/testing/__init__.py +18 -0
- crackerjack/services/patterns/testing/error_patterns.py +107 -0
- crackerjack/services/patterns/testing/pytest_output.py +126 -0
- crackerjack/services/patterns/tool_output/__init__.py +16 -0
- crackerjack/services/patterns/tool_output/bandit.py +72 -0
- crackerjack/services/patterns/tool_output/other.py +97 -0
- crackerjack/services/patterns/tool_output/pyright.py +67 -0
- crackerjack/services/patterns/tool_output/ruff.py +44 -0
- crackerjack/services/patterns/url_sanitization.py +114 -0
- crackerjack/services/patterns/utilities.py +42 -0
- crackerjack/services/patterns/utils.py +339 -0
- crackerjack/services/patterns/validation.py +46 -0
- crackerjack/services/patterns/versioning.py +62 -0
- crackerjack/services/predictive_analytics.py +21 -8
- crackerjack/services/profiler.py +280 -0
- crackerjack/services/quality/README.md +415 -0
- crackerjack/services/quality/__init__.py +11 -0
- crackerjack/services/quality/anomaly_detector.py +392 -0
- crackerjack/services/quality/pattern_cache.py +333 -0
- crackerjack/services/quality/pattern_detector.py +479 -0
- crackerjack/services/quality/qa_orchestrator.py +491 -0
- crackerjack/services/{quality_baseline.py → quality/quality_baseline.py} +163 -2
- crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
- crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
- crackerjack/services/regex_patterns.py +58 -2987
- crackerjack/services/regex_utils.py +55 -29
- crackerjack/services/secure_status_formatter.py +42 -15
- crackerjack/services/secure_subprocess.py +35 -2
- crackerjack/services/security.py +16 -8
- crackerjack/services/server_manager.py +40 -51
- crackerjack/services/smart_scheduling.py +46 -6
- crackerjack/services/status_authentication.py +3 -3
- crackerjack/services/thread_safe_status_collector.py +1 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +9 -5
- crackerjack/services/unified_config.py +43 -351
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +6 -4
- crackerjack/services/version_checker.py +14 -8
- crackerjack/services/zuban_lsp_service.py +5 -4
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/init.md +2 -12
- crackerjack/slash_commands/run.md +84 -50
- crackerjack/tools/README.md +11 -0
- crackerjack/tools/__init__.py +30 -0
- crackerjack/tools/_git_utils.py +105 -0
- crackerjack/tools/check_added_large_files.py +139 -0
- crackerjack/tools/check_ast.py +105 -0
- crackerjack/tools/check_json.py +103 -0
- crackerjack/tools/check_jsonschema.py +297 -0
- crackerjack/tools/check_toml.py +103 -0
- crackerjack/tools/check_yaml.py +110 -0
- crackerjack/tools/codespell_wrapper.py +72 -0
- crackerjack/tools/end_of_file_fixer.py +202 -0
- crackerjack/tools/format_json.py +128 -0
- crackerjack/tools/mdformat_wrapper.py +114 -0
- crackerjack/tools/trailing_whitespace.py +198 -0
- crackerjack/tools/validate_regex_patterns.py +7 -3
- crackerjack/ui/README.md +11 -0
- crackerjack/ui/dashboard_renderer.py +28 -0
- crackerjack/ui/templates/README.md +11 -0
- crackerjack/utils/console_utils.py +13 -0
- crackerjack/utils/dependency_guard.py +230 -0
- crackerjack/utils/retry_utils.py +275 -0
- crackerjack/workflows/README.md +590 -0
- crackerjack/workflows/__init__.py +46 -0
- crackerjack/workflows/actions.py +811 -0
- crackerjack/workflows/auto_fix.py +444 -0
- crackerjack/workflows/container_builder.py +499 -0
- crackerjack/workflows/definitions.py +443 -0
- crackerjack/workflows/engine.py +177 -0
- crackerjack/workflows/event_bridge.py +242 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack/managers/test_manager_backup.py +0 -1075
- crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
- crackerjack/mixins/__init__.py +0 -3
- crackerjack/mixins/error_handling.py +0 -145
- crackerjack/services/config.py +0 -358
- crackerjack/ui/server_panels.py +0 -125
- crackerjack-0.37.9.dist-info/RECORD +0 -231
- /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
- /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
"""Core error handling decorators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import errno
|
|
7
|
+
import inspect
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import typing as t
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from concurrent.futures import TimeoutError as FutureTimeoutError
|
|
13
|
+
from contextlib import suppress
|
|
14
|
+
from functools import wraps
|
|
15
|
+
|
|
16
|
+
from acb.console import Console
|
|
17
|
+
from acb.depends import depends
|
|
18
|
+
|
|
19
|
+
from ..errors import (
|
|
20
|
+
CrackerjackError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
)
|
|
23
|
+
from ..errors import (
|
|
24
|
+
TimeoutError as CrackerjackTimeoutError,
|
|
25
|
+
)
|
|
26
|
+
from .helpers import format_exception_chain, get_function_context, is_async_function
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_would_block_error(e: Exception) -> bool:
|
|
30
|
+
"""Check if exception is a would-block error (EAGAIN/EWOULDBLOCK)."""
|
|
31
|
+
err_no = getattr(e, "errno", None)
|
|
32
|
+
if err_no is not None:
|
|
33
|
+
return err_no in {errno.EAGAIN, errno.EWOULDBLOCK}
|
|
34
|
+
return isinstance(e, BlockingIOError)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fallback_stderr_write(message: str, include_traceback: bool) -> None:
|
|
38
|
+
"""Write to stderr as final fallback, suppressing all errors."""
|
|
39
|
+
with suppress(Exception):
|
|
40
|
+
err = sys.__stderr__
|
|
41
|
+
if err is not None:
|
|
42
|
+
err.write(message + "\n")
|
|
43
|
+
if include_traceback:
|
|
44
|
+
err.write("(traceback suppressed due to I/O constraints)\n")
|
|
45
|
+
err.flush()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _safe_console_print(
|
|
49
|
+
console: Console,
|
|
50
|
+
message: str,
|
|
51
|
+
*,
|
|
52
|
+
include_traceback: bool = False,
|
|
53
|
+
retries: int = 3,
|
|
54
|
+
retry_delay: float = 0.05,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Safely print to a Rich Console, tolerating non-blocking streams.
|
|
57
|
+
|
|
58
|
+
Some environments set stdout/stderr to non-blocking (e.g., PTY pipes),
|
|
59
|
+
which can raise BlockingIOError (EWOULDBLOCK/EAGAIN). This helper retries
|
|
60
|
+
briefly and then degrades silently if output still cannot be written.
|
|
61
|
+
"""
|
|
62
|
+
for attempt in range(retries + 1):
|
|
63
|
+
try:
|
|
64
|
+
console.print(message)
|
|
65
|
+
if include_traceback:
|
|
66
|
+
console.print_exception()
|
|
67
|
+
return
|
|
68
|
+
except (BlockingIOError, BrokenPipeError, OSError) as e: # pragma: no cover
|
|
69
|
+
if _is_would_block_error(e) and attempt < retries:
|
|
70
|
+
time.sleep(retry_delay)
|
|
71
|
+
continue
|
|
72
|
+
_fallback_stderr_write(message, include_traceback)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _handle_exception(
|
|
77
|
+
e: Exception,
|
|
78
|
+
func: t.Callable[..., t.Any],
|
|
79
|
+
transform_to: type[CrackerjackError] | None,
|
|
80
|
+
fallback: t.Any,
|
|
81
|
+
suppress: bool,
|
|
82
|
+
console: Console,
|
|
83
|
+
) -> t.Any:
|
|
84
|
+
"""Helper to handle exception with transformation and fallback logic."""
|
|
85
|
+
context = get_function_context(func)
|
|
86
|
+
|
|
87
|
+
# Log error with context
|
|
88
|
+
_safe_console_print(
|
|
89
|
+
console,
|
|
90
|
+
f"[red]❌ Error in {context['function_name']}: {type(e).__name__}: {e}[/red]",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Transform to CrackerjackError if requested
|
|
94
|
+
if transform_to:
|
|
95
|
+
transformed = transform_to( # type: ignore[call-arg]
|
|
96
|
+
message=str(e),
|
|
97
|
+
details={
|
|
98
|
+
"original_error": type(e).__name__,
|
|
99
|
+
"function": context["function_name"],
|
|
100
|
+
"module": context["module"],
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
if not suppress:
|
|
104
|
+
raise transformed from e
|
|
105
|
+
|
|
106
|
+
# Use fallback if provided
|
|
107
|
+
if fallback is not None:
|
|
108
|
+
return fallback() if callable(fallback) else fallback
|
|
109
|
+
|
|
110
|
+
# Re-raise if not suppressed and no transform
|
|
111
|
+
if not suppress:
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def handle_errors(
|
|
118
|
+
func: t.Callable[..., t.Any] | None = None,
|
|
119
|
+
*,
|
|
120
|
+
error_types: list[type[Exception]] | None = None,
|
|
121
|
+
fallback: t.Any = None,
|
|
122
|
+
transform_to: type[CrackerjackError] | None = None,
|
|
123
|
+
console: Console | None = None,
|
|
124
|
+
suppress: bool = False,
|
|
125
|
+
) -> (
|
|
126
|
+
t.Callable[..., t.Any]
|
|
127
|
+
| t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]
|
|
128
|
+
):
|
|
129
|
+
"""
|
|
130
|
+
Centralized error handling with transformation and fallback support.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
error_types: List of exception types to handle (None = all exceptions)
|
|
134
|
+
fallback: Fallback value or callable to return on error
|
|
135
|
+
transform_to: Transform caught exceptions to this CrackerjackError type
|
|
136
|
+
console: Optional Rich Console for error output
|
|
137
|
+
suppress: If True, suppress errors and use fallback (no re-raise)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Decorated function with error handling
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> from crackerjack.errors import FileError, ExecutionError
|
|
144
|
+
>>>
|
|
145
|
+
>>> @handle_errors(
|
|
146
|
+
... error_types=[FileNotFoundError, PermissionError],
|
|
147
|
+
... transform_to=FileError,
|
|
148
|
+
... fallback={}
|
|
149
|
+
... )
|
|
150
|
+
>>> def load_config(path: str) -> dict:
|
|
151
|
+
... with open(path) as f:
|
|
152
|
+
... return json.load(f)
|
|
153
|
+
|
|
154
|
+
>>> @handle_errors(fallback=lambda: [], suppress=True)
|
|
155
|
+
>>> def get_optional_data() -> list[str]:
|
|
156
|
+
... # Errors suppressed, returns []
|
|
157
|
+
... return fetch_data()
|
|
158
|
+
|
|
159
|
+
Notes:
|
|
160
|
+
- If transform_to is set, exceptions are wrapped in CrackerjackError
|
|
161
|
+
- Fallback can be a value or callable
|
|
162
|
+
- With suppress=True, errors are logged but not raised
|
|
163
|
+
- Integrates with Rich console for beautiful output
|
|
164
|
+
"""
|
|
165
|
+
_console = console or depends.get_sync(Console)
|
|
166
|
+
_error_types = tuple(error_types) if error_types else (Exception,)
|
|
167
|
+
|
|
168
|
+
def decorator(inner_func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
169
|
+
target = inner_func
|
|
170
|
+
if is_async_function(inner_func):
|
|
171
|
+
|
|
172
|
+
@wraps(inner_func)
|
|
173
|
+
async def async_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
174
|
+
try:
|
|
175
|
+
return await target(*args, **kwargs)
|
|
176
|
+
except _error_types as e:
|
|
177
|
+
return _handle_exception(
|
|
178
|
+
e, target, transform_to, fallback, suppress, _console
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return async_wrapper
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
|
|
185
|
+
@wraps(inner_func)
|
|
186
|
+
def sync_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
187
|
+
try:
|
|
188
|
+
return target(*args, **kwargs)
|
|
189
|
+
except _error_types as e:
|
|
190
|
+
return _handle_exception(
|
|
191
|
+
e, target, transform_to, fallback, suppress, _console
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return sync_wrapper
|
|
195
|
+
|
|
196
|
+
if func is not None and callable(func):
|
|
197
|
+
return decorator(func)
|
|
198
|
+
|
|
199
|
+
return decorator
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def log_errors(
|
|
203
|
+
logger: t.Any | None = None,
|
|
204
|
+
level: str = "error",
|
|
205
|
+
include_traceback: bool = True,
|
|
206
|
+
console: Console | None = None,
|
|
207
|
+
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
208
|
+
"""
|
|
209
|
+
Log errors with context before re-raising.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
logger: Logger instance (uses print if None)
|
|
213
|
+
level: Log level (error, warning, info, debug)
|
|
214
|
+
include_traceback: Include full exception traceback
|
|
215
|
+
console: Optional Rich Console
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Decorated function with error logging
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
>>> import logging
|
|
222
|
+
>>> logger = logging.getLogger(__name__)
|
|
223
|
+
>>>
|
|
224
|
+
>>> @log_errors(logger=logger, level="error")
|
|
225
|
+
>>> async def critical_operation() -> bool:
|
|
226
|
+
... # Errors are logged before re-raising
|
|
227
|
+
... return await perform_operation()
|
|
228
|
+
|
|
229
|
+
Notes:
|
|
230
|
+
- Does not suppress errors, only logs them
|
|
231
|
+
- Includes function context in logs
|
|
232
|
+
- Supports structured logging if logger supports it
|
|
233
|
+
"""
|
|
234
|
+
_console = console or depends.get_sync(Console)
|
|
235
|
+
|
|
236
|
+
def _log_exception(
|
|
237
|
+
func: t.Callable[..., t.Any],
|
|
238
|
+
exception: Exception,
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Log exception with function context."""
|
|
241
|
+
context = get_function_context(func)
|
|
242
|
+
error_chain = format_exception_chain(exception)
|
|
243
|
+
|
|
244
|
+
if logger:
|
|
245
|
+
log_method = getattr(logger, level, logger.error)
|
|
246
|
+
log_method(
|
|
247
|
+
f"Error in {context['function_name']}",
|
|
248
|
+
exc_info=include_traceback,
|
|
249
|
+
extra={
|
|
250
|
+
"function": context["function_name"],
|
|
251
|
+
"module": context["module"],
|
|
252
|
+
"error_type": type(exception).__name__,
|
|
253
|
+
"error_chain": error_chain,
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
_safe_console_print(
|
|
258
|
+
_console,
|
|
259
|
+
f"[red]Error in {context['function_name']}: "
|
|
260
|
+
f"{type(exception).__name__}: {exception}[/red]",
|
|
261
|
+
include_traceback=include_traceback,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
265
|
+
if is_async_function(func):
|
|
266
|
+
|
|
267
|
+
@wraps(func)
|
|
268
|
+
async def async_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
269
|
+
try:
|
|
270
|
+
return await func(*args, **kwargs)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
_log_exception(func, e)
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
return async_wrapper
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
|
|
279
|
+
@wraps(func)
|
|
280
|
+
def sync_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
281
|
+
try:
|
|
282
|
+
return func(*args, **kwargs)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
_log_exception(func, e)
|
|
285
|
+
raise
|
|
286
|
+
|
|
287
|
+
return sync_wrapper
|
|
288
|
+
|
|
289
|
+
return decorator
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _calculate_retry_delay(attempt: int, backoff: float) -> float:
|
|
293
|
+
"""Calculate delay for retry attempt with linear backoff."""
|
|
294
|
+
return backoff * attempt if backoff > 0 else 0.0
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _create_async_retry_wrapper(
|
|
298
|
+
func: t.Callable[..., t.Any],
|
|
299
|
+
max_attempts: int,
|
|
300
|
+
retry_exceptions: tuple[type[Exception], ...],
|
|
301
|
+
backoff: float,
|
|
302
|
+
) -> t.Callable[..., t.Any]:
|
|
303
|
+
"""Create async wrapper with retry logic."""
|
|
304
|
+
|
|
305
|
+
@wraps(func)
|
|
306
|
+
async def async_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
307
|
+
attempt = 0
|
|
308
|
+
while True:
|
|
309
|
+
try:
|
|
310
|
+
return await func(*args, **kwargs)
|
|
311
|
+
except retry_exceptions:
|
|
312
|
+
attempt += 1
|
|
313
|
+
if attempt >= max_attempts:
|
|
314
|
+
raise
|
|
315
|
+
delay = _calculate_retry_delay(attempt, backoff)
|
|
316
|
+
if delay > 0:
|
|
317
|
+
await asyncio.sleep(delay)
|
|
318
|
+
|
|
319
|
+
return async_wrapper
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _create_sync_retry_wrapper(
|
|
323
|
+
func: t.Callable[..., t.Any],
|
|
324
|
+
max_attempts: int,
|
|
325
|
+
retry_exceptions: tuple[type[Exception], ...],
|
|
326
|
+
backoff: float,
|
|
327
|
+
) -> t.Callable[..., t.Any]:
|
|
328
|
+
"""Create sync wrapper with retry logic."""
|
|
329
|
+
|
|
330
|
+
@wraps(func)
|
|
331
|
+
def sync_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
332
|
+
attempt = 0
|
|
333
|
+
while True:
|
|
334
|
+
try:
|
|
335
|
+
return func(*args, **kwargs)
|
|
336
|
+
except retry_exceptions:
|
|
337
|
+
attempt += 1
|
|
338
|
+
if attempt >= max_attempts:
|
|
339
|
+
raise
|
|
340
|
+
delay = _calculate_retry_delay(attempt, backoff)
|
|
341
|
+
if delay > 0:
|
|
342
|
+
time.sleep(delay)
|
|
343
|
+
|
|
344
|
+
return sync_wrapper
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def retry(
|
|
348
|
+
*,
|
|
349
|
+
max_attempts: int = 3,
|
|
350
|
+
exceptions: t.Iterable[type[Exception]] | None = None,
|
|
351
|
+
backoff: float = 0.0,
|
|
352
|
+
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
353
|
+
"""
|
|
354
|
+
Retry decorator supporting sync and async callables.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
max_attempts: Maximum number of attempts (>=1)
|
|
358
|
+
exceptions: Iterable of exception types to retry on (default: Exception)
|
|
359
|
+
backoff: Base delay in seconds applied after each failed attempt.
|
|
360
|
+
Delay grows linearly with attempt number.
|
|
361
|
+
"""
|
|
362
|
+
if max_attempts < 1:
|
|
363
|
+
raise ValueError("max_attempts must be >= 1")
|
|
364
|
+
|
|
365
|
+
retry_exceptions = tuple(exceptions) if exceptions else (Exception,)
|
|
366
|
+
|
|
367
|
+
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
368
|
+
if is_async_function(func):
|
|
369
|
+
return _create_async_retry_wrapper(
|
|
370
|
+
func, max_attempts, retry_exceptions, backoff
|
|
371
|
+
)
|
|
372
|
+
return _create_sync_retry_wrapper(func, max_attempts, retry_exceptions, backoff)
|
|
373
|
+
|
|
374
|
+
return decorator
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def with_timeout(
|
|
378
|
+
*,
|
|
379
|
+
seconds: float,
|
|
380
|
+
error_message: str | None = None,
|
|
381
|
+
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
382
|
+
"""
|
|
383
|
+
Timeout enforcement decorator.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
seconds: Maximum execution time in seconds.
|
|
387
|
+
error_message: Optional custom error message for timeout.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
391
|
+
if is_async_function(func):
|
|
392
|
+
|
|
393
|
+
@wraps(func)
|
|
394
|
+
async def async_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
395
|
+
try:
|
|
396
|
+
return await asyncio.wait_for(
|
|
397
|
+
func(*args, **kwargs), timeout=seconds
|
|
398
|
+
)
|
|
399
|
+
except TimeoutError as exc:
|
|
400
|
+
message = error_message or f"Operation timed out after {seconds}s"
|
|
401
|
+
raise CrackerjackTimeoutError(message=message) from exc
|
|
402
|
+
|
|
403
|
+
return async_wrapper
|
|
404
|
+
|
|
405
|
+
@wraps(func)
|
|
406
|
+
def sync_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
407
|
+
with ThreadPoolExecutor(max_workers=1) as executor:
|
|
408
|
+
future = executor.submit(func, *args, **kwargs)
|
|
409
|
+
try:
|
|
410
|
+
return future.result(timeout=seconds)
|
|
411
|
+
except FutureTimeoutError as exc:
|
|
412
|
+
message = error_message or f"Operation timed out after {seconds}s"
|
|
413
|
+
raise CrackerjackTimeoutError(message=message) from exc
|
|
414
|
+
|
|
415
|
+
return sync_wrapper
|
|
416
|
+
|
|
417
|
+
return decorator
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _execute_single_validator(
|
|
421
|
+
validate: t.Callable[[t.Any], bool], param: str, value: t.Any
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Execute a single validator and raise ValidationError if it fails."""
|
|
424
|
+
try:
|
|
425
|
+
result = validate(value)
|
|
426
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
427
|
+
raise ValidationError(
|
|
428
|
+
message=f"Validator for '{param}' raised {type(exc).__name__}: {exc}",
|
|
429
|
+
) from exc
|
|
430
|
+
if not result:
|
|
431
|
+
raise ValidationError(
|
|
432
|
+
message=f"Validation failed for parameter '{param}'",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _check_type_annotation_against_signature(
|
|
437
|
+
name: str, value: t.Any, signature: inspect.Signature
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Check if value matches parameter type annotation."""
|
|
440
|
+
parameter = signature.parameters.get(name)
|
|
441
|
+
if not parameter or parameter.annotation is inspect.Signature.empty:
|
|
442
|
+
return
|
|
443
|
+
if not isinstance(value, parameter.annotation): # type: ignore[arg-type]
|
|
444
|
+
raise ValidationError(
|
|
445
|
+
message=(
|
|
446
|
+
f"Parameter '{name}' expected "
|
|
447
|
+
f"{parameter.annotation!r}, got {type(value)!r}"
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _create_async_validation_wrapper(
|
|
453
|
+
func: t.Callable[..., t.Any],
|
|
454
|
+
signature: inspect.Signature,
|
|
455
|
+
validate_fn: t.Callable[[inspect.BoundArguments], None],
|
|
456
|
+
) -> t.Callable[..., t.Any]:
|
|
457
|
+
"""Create async wrapper with validation."""
|
|
458
|
+
|
|
459
|
+
@wraps(func)
|
|
460
|
+
async def async_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
461
|
+
bound = signature.bind_partial(*args, **kwargs)
|
|
462
|
+
bound.apply_defaults()
|
|
463
|
+
validate_fn(bound)
|
|
464
|
+
return await func(*args, **kwargs)
|
|
465
|
+
|
|
466
|
+
return async_wrapper
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _create_sync_validation_wrapper(
|
|
470
|
+
func: t.Callable[..., t.Any],
|
|
471
|
+
signature: inspect.Signature,
|
|
472
|
+
validate_fn: t.Callable[[inspect.BoundArguments], None],
|
|
473
|
+
) -> t.Callable[..., t.Any]:
|
|
474
|
+
"""Create sync wrapper with validation."""
|
|
475
|
+
|
|
476
|
+
@wraps(func)
|
|
477
|
+
def sync_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
478
|
+
bound = signature.bind_partial(*args, **kwargs)
|
|
479
|
+
bound.apply_defaults()
|
|
480
|
+
validate_fn(bound)
|
|
481
|
+
return func(*args, **kwargs)
|
|
482
|
+
|
|
483
|
+
return sync_wrapper
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _normalize_validators(
|
|
487
|
+
param: str,
|
|
488
|
+
funcs: t.Callable[[t.Any], bool] | t.Iterable[t.Callable[[t.Any], bool]],
|
|
489
|
+
) -> list[t.Callable[[t.Any], bool]]:
|
|
490
|
+
"""Convert single validator or iterable to list of validators."""
|
|
491
|
+
if isinstance(funcs, (list, tuple, set)):
|
|
492
|
+
return list(funcs) # type: ignore[arg-type]
|
|
493
|
+
return [funcs] # type: ignore[list-item]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _create_validator_runner(
|
|
497
|
+
validator_map: dict[
|
|
498
|
+
str, t.Callable[[t.Any], bool] | t.Iterable[t.Callable[[t.Any], bool]]
|
|
499
|
+
],
|
|
500
|
+
) -> t.Callable[[str, t.Any], None]:
|
|
501
|
+
"""Create function to run validators for a parameter."""
|
|
502
|
+
|
|
503
|
+
def _run_validators(param: str, value: t.Any) -> None:
|
|
504
|
+
funcs = validator_map.get(param)
|
|
505
|
+
if not funcs:
|
|
506
|
+
return
|
|
507
|
+
normalized = _normalize_validators(param, funcs)
|
|
508
|
+
for validate in normalized:
|
|
509
|
+
_execute_single_validator(validate, param, value)
|
|
510
|
+
|
|
511
|
+
return _run_validators
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def validate_args(
|
|
515
|
+
*,
|
|
516
|
+
validators: dict[
|
|
517
|
+
str, t.Callable[[t.Any], bool] | t.Iterable[t.Callable[[t.Any], bool]]
|
|
518
|
+
]
|
|
519
|
+
| None = None,
|
|
520
|
+
type_check: bool = False,
|
|
521
|
+
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
522
|
+
"""
|
|
523
|
+
Validate function arguments using provided callables and optional type checks.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
validators: Mapping of argument names to validator callable(s).
|
|
527
|
+
type_check: If True, enforce annotations via isinstance checks.
|
|
528
|
+
"""
|
|
529
|
+
validator_map = validators or {}
|
|
530
|
+
_run_validators = _create_validator_runner(validator_map)
|
|
531
|
+
|
|
532
|
+
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
533
|
+
signature = inspect.signature(func)
|
|
534
|
+
|
|
535
|
+
def _validate(bound: inspect.BoundArguments) -> None:
|
|
536
|
+
if type_check:
|
|
537
|
+
for name, value in bound.arguments.items():
|
|
538
|
+
_check_type_annotation_against_signature(name, value, signature)
|
|
539
|
+
|
|
540
|
+
for name, value in bound.arguments.items():
|
|
541
|
+
_run_validators(name, value)
|
|
542
|
+
|
|
543
|
+
if is_async_function(func):
|
|
544
|
+
return _create_async_validation_wrapper(func, signature, _validate)
|
|
545
|
+
|
|
546
|
+
return _create_sync_validation_wrapper(func, signature, _validate)
|
|
547
|
+
|
|
548
|
+
return decorator
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _handle_degradation_error(
|
|
552
|
+
func: t.Callable[..., t.Any],
|
|
553
|
+
e: Exception,
|
|
554
|
+
fallback_value: t.Any,
|
|
555
|
+
warn: bool,
|
|
556
|
+
console: Console,
|
|
557
|
+
) -> t.Any:
|
|
558
|
+
"""Handle error with warning and fallback resolution."""
|
|
559
|
+
if warn:
|
|
560
|
+
context = get_function_context(func)
|
|
561
|
+
_safe_console_print(
|
|
562
|
+
console,
|
|
563
|
+
f"[yellow]⚠️ {context['function_name']} failed, using fallback: "
|
|
564
|
+
f"{type(e).__name__}[/yellow]",
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if callable(fallback_value):
|
|
568
|
+
return fallback_value()
|
|
569
|
+
return fallback_value
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _create_async_degradation_wrapper(
|
|
573
|
+
func: t.Callable[..., t.Any],
|
|
574
|
+
fallback_value: t.Any,
|
|
575
|
+
warn: bool,
|
|
576
|
+
console: Console,
|
|
577
|
+
) -> t.Callable[..., t.Any]:
|
|
578
|
+
"""Create async wrapper with graceful degradation."""
|
|
579
|
+
|
|
580
|
+
@wraps(func)
|
|
581
|
+
async def async_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
582
|
+
try:
|
|
583
|
+
return await func(*args, **kwargs)
|
|
584
|
+
except Exception as e:
|
|
585
|
+
return _handle_degradation_error(func, e, fallback_value, warn, console)
|
|
586
|
+
|
|
587
|
+
return async_wrapper
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _create_sync_degradation_wrapper(
|
|
591
|
+
func: t.Callable[..., t.Any],
|
|
592
|
+
fallback_value: t.Any,
|
|
593
|
+
warn: bool,
|
|
594
|
+
console: Console,
|
|
595
|
+
) -> t.Callable[..., t.Any]:
|
|
596
|
+
"""Create sync wrapper with graceful degradation."""
|
|
597
|
+
|
|
598
|
+
@wraps(func)
|
|
599
|
+
def sync_wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
600
|
+
try:
|
|
601
|
+
return func(*args, **kwargs)
|
|
602
|
+
except Exception as e:
|
|
603
|
+
return _handle_degradation_error(func, e, fallback_value, warn, console)
|
|
604
|
+
|
|
605
|
+
return sync_wrapper
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def graceful_degradation(
|
|
609
|
+
fallback_value: t.Any = None,
|
|
610
|
+
warn: bool = True,
|
|
611
|
+
console: Console | None = None,
|
|
612
|
+
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
613
|
+
"""
|
|
614
|
+
Gracefully degrade on errors with optional warnings.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
fallback_value: Value to return on error (can be callable)
|
|
618
|
+
warn: Show warning message when falling back
|
|
619
|
+
console: Optional Rich Console
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
Decorated function with graceful degradation
|
|
623
|
+
|
|
624
|
+
Example:
|
|
625
|
+
>>> @graceful_degradation(fallback_value=[], warn=True)
|
|
626
|
+
>>> def get_optional_features() -> list[str]:
|
|
627
|
+
... # Returns [] on error with warning
|
|
628
|
+
... return fetch_features()
|
|
629
|
+
|
|
630
|
+
>>> @graceful_degradation(fallback_value=lambda: {})
|
|
631
|
+
>>> async def load_cache() -> dict:
|
|
632
|
+
... # Returns {} on error
|
|
633
|
+
... return await load_cache_file()
|
|
634
|
+
|
|
635
|
+
Notes:
|
|
636
|
+
- Suppresses all exceptions
|
|
637
|
+
- Logs/warns about failures if warn=True
|
|
638
|
+
- Useful for optional features that shouldn't break the app
|
|
639
|
+
"""
|
|
640
|
+
_console = console or depends.get_sync(Console)
|
|
641
|
+
|
|
642
|
+
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
643
|
+
if is_async_function(func):
|
|
644
|
+
return _create_async_degradation_wrapper(
|
|
645
|
+
func, fallback_value, warn, _console
|
|
646
|
+
)
|
|
647
|
+
return _create_sync_degradation_wrapper(func, fallback_value, warn, _console)
|
|
648
|
+
|
|
649
|
+
return decorator
|